Ansible-Roles/roles/firewall/library/firewall_lib.py
a.kazantsev 9bfaa8489a fix
2026-07-11 01:56:08 +03:00

3731 lines
142 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 - 2026 Red Hat, Inc.
# Reusing some firewalld code
# Authors:
# Thomas Woerner <twoerner@redhat.com>
# Martin Pitt <mpitt@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
ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}
DOCUMENTATION = """
---
module: firewall_lib
short_description: Module for firewall role
requirements:
- python3-firewall or python-firewall
description:
Manage firewall with firewalld on Fedora and RHEL-7+.
author: "Thomas Woerner (@t-woerner), Martin Pitt (@martinpitt)"
options:
firewalld_conf:
description:
Modify firewalld.conf directives
suboptions:
allow_zone_drifting:
description:
Set AllowZoneDrifting directive if not deprecated
required: false
type: bool
required: false
type: dict
service:
description:
List of service name strings.
The service names needs to be defined in firewalld configuration.
services in firewalld configuration can be defined by setting
this option to a single service name and state to present.
required: false
type: list
elements: str
default: []
port:
description:
List of ports or port range strings.
The format of a port needs to be port=<port>[-<port>]/<protocol>.
required: false
type: list
elements: str
default: []
source_port:
description:
List of source port or port range strings.
The format of a source port needs to be port=<port>[-<port>]/<protocol>.
required: false
type: list
elements: str
default: []
forward_port:
description:
List of forward port strings or dicts,
or a single string or dict.
The format of a forward port string needs to be
<port>[-<port>]/<protocol>;[<to-port>];[<to-addr>].
aliases: ["port_forward"]
required: false
type: raw
default: []
masquerade:
description:
The masquerade bool setting.
type: bool
rich_rule:
description:
List of rich rule strings.
For the format see L(Syntax for firewalld rich language rules,
https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html).
required: false
type: list
elements: str
default: []
source:
description:
List of source address, address range strings, or ipsets
A source address or address range is either an IP address or a network
IP address with a mask for IPv4 or IPv6. For IPv4, the mask can be a
network mask or a plain number. For IPv6 the mask is a plain number.
An ipset is used by prefixing "ipset{{ ":" }}" to the defined ipset's name.
required: false
type: list
elements: str
default: []
interface:
description:
List of interface name strings.
required: false
type: list
elements: str
default: []
interface_pci_id:
description:
List of interface PCI device ID strings.
PCI device ID needs to correspond to a named network interface.
required: false
type: list
elements: str
default: []
icmp_block:
description:
List of ICMP type strings to block.
The ICMP type names needs to be defined in firewalld configuration.
required: false
type: list
elements: str
default: []
icmp_block_inversion:
description:
ICMP block inversion bool setting.
It enables or disables inversion of ICMP blocks for a zone in firewalld.
required: false
type: bool
timeout:
description:
The amount of time in seconds a setting is in effect.
The timeout is usable for services, ports, source ports, forward ports,
masquerade, rich rules or icmp blocks for runtime only.
required: false
type: int
default: 0
target:
description:
The firewalld Zone target.
If the state is set to C(absent), this will reset the target to default.
required: false
choices: ["default", "ACCEPT", "DROP", "%%REJECT%%"]
type: str
zone:
description:
The zone name string.
If the zone name is not given, then the default zone will be used.
required: false
type: str
set_default_zone:
description: Sets the default zone.
required: false
type: str
ipset:
description:
Name of the ipset being configured.
Can be used to define, modify, or remove ipsets.
Must set state to C(present) or C(absent) to use this argument.
Must set permanent to C(true) to use this argument.
required: false
type: str
ipset_type:
description:
Type of ipset being defined
Will only do something when ipset argument is defined.
To get the list of supported ipset types, use
firewall-cmd --get-ipset-types.
required: false
type: str
ipset_entries:
description:
List of addresses to add/remove from ipset.
Must be compatible with the ipset type of the `ipset`
being created or modified.
Will only do something when set with ipset.
required: false
type: list
elements: str
default: []
ipset_options:
description:
Dict of key/value pairs of ipset options for the given ipset.
Will only do something when set with ipset.
required: false
type: dict
default: {}
permanent:
description:
The permanent bool flag.
Ensures settings permanently across system reboots and firewalld
service restarts.
If the permanent flag is not enabled, runtime is assumed.
required: false
type: bool
runtime:
description:
The runtime bool flag.
Ensures settings in the runtime environment that is not persistent
across system reboots and firewalld service restarts.
aliases: ["immediate"]
required: false
type: bool
state:
description:
Ensure presence or absence of entries. Use C(present) and C(absent) only
for zone-only operations, service-only operations, or target operations.
required: false
type: str
choices: ["enabled", "disabled", "present", "absent"]
description:
description:
Creates or updates the description of a new or existing service or ipset.
State needs to be present for the use of this argument.
Supported for ipsets and services.
required: false
type: str
short:
description:
Creates or updates a short description, generally just a full name of a
new or existing service.
Supported for custom services and ipsets while state is present
required: false
type: str
protocol:
description:
list of protocols supported by managed system.
Supported for service configuration only
required: false
type: list
elements: str
default: []
helper_module:
description:
List of netfiler kernel helper module names
required: false
type: list
elements: str
default: []
destination:
description:
List of IPv4/IPv6 addresses with optional mask
format - address[/mask]
Currently only supported for service configuration
Only one IPv4 and one IPv6 address allowed in list.
required: false
type: list
elements: str
default: []
previous:
description:
The previous state of the firewall configuration.
The value "replaced" means that the entire firewall configuration will be replaced with the new configuration.
required: false
type: str
choices: ["replaced", "kept"]
default: "kept"
includes:
description:
Services to include in this one.
required: false
type: list
elements: str
default: []
online:
description:
When true, use the D-Bus API to query the status from the running system.
Otherwise, use firewall-offline-cmd(1). Offline mode is
incompatible with "runtime" mode.
type: bool
required: false
default: true
__called_from_role:
description:
If true, the module is being called from the role.
type: bool
required: false
default: false
config_list:
description:
List of firewall configurations to apply.
Each item in the list is a dictionary containing any of the module's
parameters (except config_list itself).
This allows applying multiple firewall configurations in a single
module call. Cannot be used together with other module parameters.
type: list
elements: dict
required: false
default: []
suboptions:
firewalld_conf:
description:
Modify firewalld.conf directives
suboptions:
allow_zone_drifting:
description:
Set AllowZoneDrifting directive if not deprecated
required: false
type: bool
required: false
type: dict
service:
description:
List of service name strings.
The service names needs to be defined in firewalld configuration.
services in firewalld configuration can be defined by setting
this option to a single service name and state to present.
required: false
type: list
elements: str
default: []
port:
description:
List of ports or port range strings.
The format of a port needs to be port=<port>[-<port>]/<protocol>.
required: false
type: list
elements: str
default: []
source_port:
description:
List of source port or port range strings.
The format of a source port needs to be port=<port>[-<port>]/<protocol>.
required: false
type: list
elements: str
default: []
forward_port:
description:
List of forward port strings or dicts,
or a single string or dict.
The format of a forward port string needs to be
<port>[-<port>]/<protocol>;[<to-port>];[<to-addr>].
aliases: ["port_forward"]
required: false
type: raw
default: []
masquerade:
description:
The masquerade bool setting.
type: bool
rich_rule:
description:
List of rich rule strings.
For the format see L(Syntax for firewalld rich language rules,
https://firewalld.org/documentation/man-pages/firewalld.richlanguage.html).
required: false
type: list
elements: str
default: []
source:
description:
List of source address, address range strings, or ipsets
A source address or address range is either an IP address or a network
IP address with a mask for IPv4 or IPv6. For IPv4, the mask can be a
network mask or a plain number. For IPv6 the mask is a plain number.
An ipset is used by prefixing "ipset{{ ":" }}" to the defined ipset's name.
required: false
type: list
elements: str
default: []
interface:
description:
List of interface name strings.
required: false
type: list
elements: str
default: []
interface_pci_id:
description:
List of interface PCI device ID strings.
PCI device ID needs to correspond to a named network interface.
required: false
type: list
elements: str
default: []
icmp_block:
description:
List of ICMP type strings to block.
The ICMP type names needs to be defined in firewalld configuration.
required: false
type: list
elements: str
default: []
icmp_block_inversion:
description:
ICMP block inversion bool setting.
It enables or disables inversion of ICMP blocks for a zone in firewalld.
required: false
type: bool
timeout:
description:
The amount of time in seconds a setting is in effect.
The timeout is usable for services, ports, source ports, forward ports,
masquerade, rich rules or icmp blocks for runtime only.
required: false
type: int
default: 0
target:
description:
The firewalld Zone target.
If the state is set to C(absent), this will reset the target to default.
required: false
choices: ["default", "ACCEPT", "DROP", "%%REJECT%%"]
type: str
zone:
description:
The zone name string.
If the zone name is not given, then the default zone will be used.
required: false
type: str
set_default_zone:
description: Sets the default zone.
required: false
type: str
ipset:
description:
Name of the ipset being configured.
Can be used to define, modify, or remove ipsets.
Must set state to C(present) or C(absent) to use this argument.
Must set permanent to C(true) to use this argument.
required: false
type: str
ipset_type:
description:
Type of ipset being defined
Will only do something when ipset argument is defined.
To get the list of supported ipset types, use
firewall-cmd --get-ipset-types.
required: false
type: str
ipset_entries:
description:
List of addresses to add/remove from ipset.
Must be compatible with the ipset type of the `ipset`
being created or modified.
Will only do something when set with ipset.
required: false
type: list
elements: str
default: []
ipset_options:
description:
Dict of key/value pairs of ipset options for the given ipset.
Will only do something when set with ipset.
required: false
type: dict
default: {}
permanent:
description:
The permanent bool flag.
Ensures settings permanently across system reboots and firewalld
service restarts.
If the permanent flag is not enabled, runtime is assumed.
required: false
type: bool
runtime:
description:
The runtime bool flag.
Ensures settings in the runtime environment that is not persistent
across system reboots and firewalld service restarts.
aliases: ["immediate"]
required: false
type: bool
state:
description:
Ensure presence or absence of entries. Use C(present) and C(absent) only
for zone-only operations, service-only operations, or target operations.
required: false
type: str
choices: ["enabled", "disabled", "present", "absent"]
description:
description:
Creates or updates the description of a new or existing service or ipset.
State needs to be present for the use of this argument.
Supported for ipsets and services.
required: false
type: str
short:
description:
Creates or updates a short description, generally just a full name of a
new or existing service.
Supported for custom services and ipsets while state is present
required: false
type: str
protocol:
description:
List of protocols supported by managed system.
Supported for service configuration only
required: false
type: list
elements: str
default: []
helper_module:
description:
List of netfiler kernel helper module names
required: false
type: list
elements: str
default: []
destination:
description:
List of IPv4/IPv6 addresses with optional mask
format - address[/mask]
Currently only supported for service configuration
Only one IPv4 and one IPv6 address allowed in list.
required: false
type: list
elements: str
default: []
previous:
description:
The previous state of the firewall configuration.
The value "replaced" means that the entire firewall configuration will be replaced with the new configuration.
required: false
type: str
choices: ["replaced", "kept"]
default: "kept"
includes:
description:
Services to include in this one.
required: false
type: list
elements: str
default: []
"""
EXAMPLES = """
# Single configuration (current method)
- name: Configure firewall ports
firewall_lib:
- port: ['443/tcp', '443/udp']
# Multiple configurations using config_list (new method)
- name: Configure firewall ports again with config_list
firewall_lib:
config_list:
- port: ['80/tcp']
state: enabled
permanent: true
- service: ['ssh']
state: enabled
permanent: true
- port: ['8080/tcp']
zone: public
state: enabled
runtime: true
# Each dict in config_list can contain any parameters that the module supports
- name: Configure firewall with config_list again
firewall_lib:
config_list:
- zone: public
target: ACCEPT
state: present
permanent: true
- service: ['http', 'https']
zone: public
state: enabled
permanent: true
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.fedora.linux_system_roles.plugins.module_utils.firewall_lsr.get_config import (
config_to_dict,
export_config_dict,
recursive_show_diffs,
)
import re
import os
import copy
# ansible six is deprecated, and it seems a lot to add a dependency on python-six
# just for this
try:
lsr_string_types = (basestring,)
except NameError:
lsr_string_types = (str,)
try:
import ipaddress
HAS_IPADDRESS = True
except ImportError:
HAS_IPADDRESS = False
try:
import firewall.config
FW_VERSION = firewall.config.VERSION
from firewall.client import (
FirewallClient,
Rich_Rule,
FirewallClientZoneSettings,
FirewallClientServiceSettings,
FirewallClientIPSetSettings,
)
from firewall.core.io.firewalld_conf import firewalld_conf
from firewall.core.io.zone import Zone
from firewall.core.io.service import Service
from firewall.core.io.ipset import IPSet
FIREWALLD_DIR = firewall.config.ETC_FIREWALLD
HAS_FIREWALLD = True
except ImportError:
FIREWALLD_DIR = "/etc/firewalld"
HAS_FIREWALLD = False
try:
if HAS_FIREWALLD:
firewall.config.FIREWALLD_POLICIES
HAS_POLICIES = True
except AttributeError:
HAS_POLICIES = False
try:
from firewall.functions import check_mac
HAS_CHECK_MAC = True
except ImportError:
HAS_CHECK_MAC = False
def check_mac(mac):
return False
try:
from firewall.core.fw_nm import (
nm_is_imported,
nm_get_connection_of_interface,
nm_get_zone_of_connection,
nm_set_zone_of_connection,
nm_get_interfaces,
nm_get_client,
)
NM_IMPORTED = nm_is_imported()
except ImportError:
NM_IMPORTED = False
# The argument to ip_interface must be a unicode string
# Must be "cast" in python2, python3 does not need this
def ip_interface(entry, module):
try:
entry_str = unicode(entry)
except NameError: # unicode is not defined in python3
entry_str = entry
try:
return ipaddress.ip_interface(entry_str)
except ValueError:
module.fail_json(msg="Invalid IP address - " + entry)
return None
def try_get_connection_of_interface(interface):
try:
return nm_get_connection_of_interface(interface)
except Exception:
return None
def try_set_zone_of_interface(module, _zone, interface):
if NM_IMPORTED:
connection = try_get_connection_of_interface(interface)
if connection is not None:
if _zone == "":
zone_string = "the default zone"
else:
zone_string = _zone
if _zone == nm_get_zone_of_connection(connection):
module.log(
msg="The interface is under control of NetworkManager and already bound to '%s'"
% zone_string
)
return (True, False)
else:
if not module.check_mode:
nm_set_zone_of_connection(_zone, connection)
return (True, True)
return (False, False)
# Check that all of the ipset entries are ipv4, ipv6, or mac addresses
# if not, fail the module
# if they are, return "ipv4", "ipv6", or "mac"
def get_ipset_entries_type(ipset_entries, module):
addr_type = None
is_mixed_addr_types = False
for entry in ipset_entries:
if check_mac(entry):
if addr_type is None:
addr_type = "mac"
elif addr_type != "mac":
is_mixed_addr_types = True
else:
if HAS_IPADDRESS:
addr = ip_interface(entry, module)
# ip_interface will fail the module if the address is invalid
if addr is None:
continue
else:
module.fail_json(msg="No IP address library found")
continue
if addr.version == 4:
if addr_type is None:
addr_type = "ipv4"
elif addr_type != "ipv4":
is_mixed_addr_types = True
elif addr.version == 6:
if addr_type is None:
addr_type = "ipv6"
elif addr_type != "ipv6":
is_mixed_addr_types = True
else:
module.fail_json(msg="Invalid IP address - " + entry)
if is_mixed_addr_types:
module.fail_json(
msg="Address types cannot be mixed in ipset entries - " + str(ipset_entries)
)
return addr_type
# ipset options values must be strings
def normalize_ipset_options(ipset_options):
for option, value in ipset_options.items():
if value is not None and not isinstance(value, lsr_string_types):
ipset_options[option] = str(value)
def check_and_normalize_ipset(module, ipset, ipset_entries, ipset_options):
addr_type = get_ipset_entries_type(ipset_entries, module)
if addr_type is None and ipset_entries:
module.fail_json(
msg="ipset %s: Invalid IP address - %s " % (ipset, str(ipset_entries))
)
copy_ipset_options = copy.deepcopy(ipset_options)
normalize_ipset_options(copy_ipset_options)
if addr_type == "ipv6" and "family" not in ipset_options:
copy_ipset_options["family"] = "inet6"
if addr_type == "ipv4" and copy_ipset_options.get("family") == "inet6":
module.fail_json(
msg="ipset %s: family=inet6 is not supported for IPv4 ipset_entries %s"
% (ipset, ", ".join(ipset_entries))
)
if addr_type == "ipv6" and copy_ipset_options.get("family") == "inet":
module.fail_json(
msg="ipset %s: family=inet is not supported for IPv6 ipset_entries %s"
% (ipset, ", ".join(ipset_entries))
)
return copy_ipset_options
# Above: adapted from firewall-cmd source code
class OnlineAPIBackend:
"""Implement operations with the FirewallClient() API.
This requires firewalld to be running.
"""
def __init__(self, module, permanent, runtime, zone, state, timeout):
self.module = module
self.state = state
self.permanent = permanent
self.runtime = runtime
self.zone = zone
self.timeout = timeout
self.set_interface_changed = False
self.fw = FirewallClient()
# Set exception handler
def exception_handler(exception_message):
module.fail_json(msg=exception_message)
self.fw.setExceptionHandler(exception_handler)
self.changed = False
self.need_reload = False
# Get default zone, the permanent zone and settings
zone_exists = False
if runtime:
zone_exists = zone_exists or zone is None or zone in self.fw.getZones()
if permanent:
zone_exists = (
zone_exists or zone is None or zone in self.fw.config().getZoneNames()
)
if zone_exists:
self.zone = self.zone or self.fw.getDefaultZone()
self.fw_zone = self.fw.config().getZoneByName(self.zone)
self.fw_settings = self.fw_zone.getSettings()
else:
self.fw_zone = None
self.fw_settings = None
zone_exists = False
self.zone_exists = zone_exists
def check_zone_exists(self):
return self.zone_exists
def finalize(self):
if self.fw_zone and self.fw_settings:
self.fw_zone.update(self.fw_settings)
if self.need_reload:
self.fw.reload()
def set_firewalld_conf(self, firewalld_conf, allow_zone_drifting_deprecated):
fw_config = self.fw.config()
if not allow_zone_drifting_deprecated and firewalld_conf.get(
"allow_zone_drifting"
) != fw_config.get_property("AllowZoneDrifting"):
if not self.module.check_mode:
fw_config.set_property(
"AllowZoneDrifting", firewalld_conf.get("allow_zone_drifting")
)
self.changed = True
self.need_reload = True
def set_zone(self):
if self.state == "present" and not self.zone_exists:
if not self.module.check_mode:
self.fw.config().addZone(self.zone, FirewallClientZoneSettings())
self.need_reload = True
self.changed = True
elif self.state == "absent" and self.zone_exists:
if not self.module.check_mode:
self.fw_zone.remove()
self.need_reload = True
self.changed = True
self.fw_zone = None
self.fw_settings = None
def set_default_zone(self, zone):
if self.fw.getDefaultZone() != zone:
self.fw.setDefaultZone(zone)
self.changed = True
def _create_service(self, service):
if not self.module.check_mode:
self.fw.config().addService(service, FirewallClientServiceSettings())
fw_service = self.fw.config().getServiceByName(service)
fw_service_settings = fw_service.getSettings()
else:
fw_service = None
fw_service_settings = FirewallClientServiceSettings()
return fw_service, fw_service_settings
def set_service(
self,
service_operation,
service,
description,
short,
port,
protocol,
source_port,
helper_module,
destination_ipv4,
destination_ipv6,
includes,
):
if service_operation and self.permanent:
service_exists = service in self.fw.config().getServiceNames()
if service_exists:
fw_service = self.fw.config().getServiceByName(service)
fw_service_settings = fw_service.getSettings()
elif self.state == "present":
fw_service, fw_service_settings = self._create_service(service)
self.changed = True
service_exists = True
if self.state == "present":
if (
description is not None
and description != fw_service_settings.getDescription()
):
if not self.module.check_mode:
fw_service_settings.setDescription(description)
self.changed = True
if short is not None and short != fw_service_settings.getShort():
if not self.module.check_mode:
fw_service_settings.setShort(short)
self.changed = True
for _port, _protocol in port:
if not fw_service_settings.queryPort(_port, _protocol):
if not self.module.check_mode:
fw_service_settings.addPort(_port, _protocol)
self.changed = True
for _protocol in protocol:
if not fw_service_settings.queryProtocol(_protocol):
if not self.module.check_mode:
fw_service_settings.addProtocol(_protocol)
self.changed = True
for _port, _protocol in source_port:
if not fw_service_settings.querySourcePort(_port, _protocol):
if not self.module.check_mode:
fw_service_settings.addSourcePort(_port, _protocol)
self.changed = True
for _module in helper_module:
if hasattr(fw_service_settings, "queryHelper"):
if not fw_service_settings.queryHelper(_module):
if not self.module.check_mode:
fw_service_settings.addHelper(_module)
self.changed = True
elif not fw_service_settings.queryModule(_module):
if not self.module.check_mode:
fw_service_settings.addModule(_module)
self.changed = True
if destination_ipv4:
if not fw_service_settings.queryDestination(
"ipv4", destination_ipv4
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv4", destination_ipv4)
self.changed = True
if destination_ipv6:
if not fw_service_settings.queryDestination(
"ipv6", destination_ipv6
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv6", destination_ipv6)
self.changed = True
for _include in includes:
if not fw_service_settings.queryInclude(_include):
if not self.module.check_mode:
fw_service_settings.addInclude(_include)
self.changed = True
if self.state == "absent" and service_exists:
for _port, _protocol in port:
if fw_service_settings.queryPort(_port, _protocol):
if not self.module.check_mode:
fw_service_settings.removePort(_port, _protocol)
self.changed = True
for _port, _protocol in source_port:
if fw_service_settings.querySourcePort(_port, _protocol):
if not self.module.check_mode:
fw_service_settings.removeSourcePort(_port, _protocol)
self.changed = True
for _protocol in protocol:
if fw_service_settings.queryProtocol(_protocol):
if not self.module.check_mode:
fw_service_settings.removeProtocol(_protocol)
self.changed = True
for _module in helper_module:
if hasattr(fw_service_settings, "queryHelper"):
if fw_service_settings.queryHelper(_module):
if not self.module.check_mode:
fw_service_settings.removeHelper(_module)
self.changed = True
elif fw_service_settings.queryModule(_module):
if not self.module.check_mode:
fw_service_settings.removeModule(_module)
self.changed = True
if destination_ipv4:
if fw_service_settings.queryDestination("ipv4", destination_ipv4):
if not self.module.check_mode:
fw_service_settings.removeDestination(
"ipv4", destination_ipv4
)
self.changed = True
if destination_ipv6:
if fw_service_settings.queryDestination("ipv6", destination_ipv6):
if not self.module.check_mode:
fw_service_settings.removeDestination(
"ipv6", destination_ipv6
)
self.changed = True
for _include in includes:
if fw_service_settings.queryInclude(_include):
if not self.module.check_mode:
fw_service_settings.removeInclude(_include)
self.changed = True
if not any(
(
port,
source_port,
protocol,
helper_module,
destination_ipv4,
destination_ipv6,
)
):
if not self.module.check_mode:
fw_service.remove()
service_exists = False
self.changed = True
# If service operation occurs, this should be the only instruction executed by the script
if self.changed and not self.module.check_mode:
if service_exists:
fw_service.update(fw_service_settings)
self.need_reload = True
else:
for item in service:
service_exists = item in self.fw.config().getServiceNames()
if self.state == "enabled" and service_exists:
if self.runtime and not self.fw.queryService(self.zone, item):
if not self.module.check_mode:
self.fw.addService(self.zone, item, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.queryService(item):
if not self.module.check_mode:
self.fw_settings.addService(item)
self.changed = True
elif self.state == "disabled" and service_exists:
if self.runtime and self.fw.queryService(self.zone, item):
if not self.module.check_mode:
self.fw.removeService(self.zone, item)
if self.permanent and self.fw_settings.queryService(item):
if not self.module.check_mode:
self.fw_settings.removeService(item)
self.changed = True
else:
if self.module.check_mode:
self.module.warn(
"Service does not exist - "
+ item
+ ". Ensure that you define the service in the playbook before running it in diff mode"
)
else:
self.module.fail_json(msg="INVALID SERVICE - " + item)
def _create_ipset(self, ipset, ipset_type):
if not ipset_type:
self.module.fail_json(msg="ipset_type needed when creating a new ipset")
fw_ipset = None
fw_ipset_settings = FirewallClientIPSetSettings()
fw_ipset_settings.setType(ipset_type)
if not self.module.check_mode:
self.fw.config().addIPSet(ipset, fw_ipset_settings)
fw_ipset = self.fw.config().getIPSetByName(ipset)
fw_ipset_settings = fw_ipset.getSettings()
return fw_ipset, fw_ipset_settings
def set_ipset(
self, ipset, description, short, ipset_type, ipset_entries, ipset_options
):
ipset_options = check_and_normalize_ipset(
self.module, ipset, ipset_entries, ipset_options
)
ipset_exists = ipset in self.fw.config().getIPSetNames()
fw_ipset = None
fw_ipset_settings = None
if ipset_exists:
fw_ipset = self.fw.config().getIPSetByName(ipset)
fw_ipset_settings = fw_ipset.getSettings()
if ipset_type and ipset_type != fw_ipset_settings.getType():
self.module.fail_json(
msg="Name conflict when creating ipset - "
"ipset %s of type %s already exists"
% (ipset, fw_ipset_settings.getType())
)
elif self.state == "present":
fw_ipset, fw_ipset_settings = self._create_ipset(ipset, ipset_type)
self.changed = True
ipset_exists = True
if self.state == "present":
if (
description is not None
and description != fw_ipset_settings.getDescription()
):
if not self.module.check_mode:
fw_ipset_settings.setDescription(description)
self.changed = True
if short is not None and short != fw_ipset_settings.getShort():
if not self.module.check_mode:
fw_ipset_settings.setShort(short)
self.changed = True
for entry in ipset_entries:
if not fw_ipset_settings.queryEntry(entry):
if not self.module.check_mode:
fw_ipset_settings.addEntry(entry)
self.changed = True
for option, value in ipset_options.items():
if value is None:
continue
current_options = fw_ipset_settings.getOptions()
if option in current_options:
if current_options[option] != value:
if not self.module.check_mode:
fw_ipset_settings.removeOption(option)
fw_ipset_settings.addOption(option, value)
self.changed = True
else:
if not self.module.check_mode:
fw_ipset_settings.addOption(option, value)
self.changed = True
elif ipset_exists:
if ipset_entries or ipset_options:
for entry in ipset_entries:
if fw_ipset_settings.queryEntry(entry):
if not self.module.check_mode:
fw_ipset_settings.removeEntry(entry)
self.changed = True
for option, value in ipset_options.items():
current_options = fw_ipset_settings.getOptions()
if (value is None and option in current_options) or (
value is not None and current_options[option] == value
):
if not self.module.check_mode:
fw_ipset_settings.removeOption(option)
self.changed = True
else:
ipset_source_name = "ipset:%s" % ipset
bound_zone_permanent = self.fw.config().getZoneOfSource(
ipset_source_name
)
if bound_zone_permanent:
bound_zone_permanent = "permanent - %s" % bound_zone_permanent
bound_zone_runtime = self.fw.getZoneOfSource(ipset_source_name)
if bound_zone_runtime:
bound_zone_runtime = "runtime - %s" % bound_zone_runtime
if bound_zone_permanent or bound_zone_runtime:
bound_zones = " | ".join(
[
i
for i in [bound_zone_permanent, bound_zone_runtime]
if i != ""
]
)
if self.module.check_mode:
self.module.warn(
"Ensure %s is removed from all zones before attempting to remove it. Enabled zones: %s"
% (ipset_source_name, bound_zones)
)
else:
self.module.fail_json(
msg="Remove %s from all permanent and runtime zones before attempting to remove it"
% ipset_source_name
)
elif ipset_exists:
if not self.module.check_mode:
fw_ipset.remove()
ipset_exists = False
self.changed = True
if self.changed and not self.module.check_mode:
if ipset_exists:
fw_ipset.update(fw_ipset_settings)
self.need_reload = True
def set_port(self, port):
for _port, _protocol in port:
if self.state == "enabled":
if self.runtime and not self.fw.queryPort(self.zone, _port, _protocol):
if not self.module.check_mode:
self.fw.addPort(self.zone, _port, _protocol, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.queryPort(_port, _protocol):
if not self.module.check_mode:
self.fw_settings.addPort(_port, _protocol)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.queryPort(self.zone, _port, _protocol):
if not self.module.check_mode:
self.fw.removePort(self.zone, _port, _protocol)
self.changed = True
if self.permanent and self.fw_settings.queryPort(_port, _protocol):
if not self.module.check_mode:
self.fw_settings.removePort(_port, _protocol)
self.changed = True
def set_source_port(self, source_port):
for _port, _protocol in source_port:
if self.state == "enabled":
if self.runtime and not self.fw.querySourcePort(
self.zone, _port, _protocol
):
if not self.module.check_mode:
self.fw.addSourcePort(self.zone, _port, _protocol, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.querySourcePort(
_port, _protocol
):
if not self.module.check_mode:
self.fw_settings.addSourcePort(_port, _protocol)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.querySourcePort(
self.zone, _port, _protocol
):
if not self.module.check_mode:
self.fw.removeSourcePort(self.zone, _port, _protocol)
self.changed = True
if self.permanent and self.fw_settings.querySourcePort(
_port, _protocol
):
if not self.module.check_mode:
self.fw_settings.removeSourcePort(_port, _protocol)
self.changed = True
def set_forward_port(self, forward_port):
for _port, _protocol, _to_port, _to_addr in forward_port:
if self.state == "enabled":
if self.runtime and not self.fw.queryForwardPort(
self.zone, _port, _protocol, _to_port, _to_addr
):
if not self.module.check_mode:
self.fw.addForwardPort(
self.zone,
_port,
_protocol,
_to_port,
_to_addr,
self.timeout,
)
self.changed = True
if self.permanent and not self.fw_settings.queryForwardPort(
_port, _protocol, _to_port, _to_addr
):
if not self.module.check_mode:
self.fw_settings.addForwardPort(
_port, _protocol, _to_port, _to_addr
)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.queryForwardPort(
self.zone, _port, _protocol, _to_port, _to_addr
):
if not self.module.check_mode:
self.fw.removeForwardPort(
self.zone, _port, _protocol, _to_port, _to_addr
)
self.changed = True
if self.permanent and self.fw_settings.queryForwardPort(
_port, _protocol, _to_port, _to_addr
):
if not self.module.check_mode:
self.fw_settings.removeForwardPort(
_port, _protocol, _to_port, _to_addr
)
self.changed = True
def set_masquerade(self, masquerade):
if masquerade:
if self.runtime and not self.fw.queryMasquerade(self.zone):
if not self.module.check_mode:
self.fw.addMasquerade(self.zone, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.queryMasquerade():
if not self.module.check_mode:
self.fw_settings.addMasquerade()
self.changed = True
else:
if self.runtime and self.fw.queryMasquerade(self.zone):
if not self.module.check_mode:
self.fw.removeMasquerade(self.zone)
self.changed = True
if self.permanent and self.fw_settings.queryMasquerade():
if not self.module.check_mode:
self.fw_settings.removeMasquerade()
self.changed = True
def set_rich_rule(self, rich_rule):
for item in rich_rule:
if self.state == "enabled":
if self.runtime and not self.fw.queryRichRule(self.zone, item):
if not self.module.check_mode:
self.fw.addRichRule(self.zone, item, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.queryRichRule(item):
if not self.module.check_mode:
self.fw_settings.addRichRule(item)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.queryRichRule(self.zone, item):
if not self.module.check_mode:
self.fw.removeRichRule(self.zone, item)
self.changed = True
if self.permanent and self.fw_settings.queryRichRule(item):
if not self.module.check_mode:
self.fw_settings.removeRichRule(item)
self.changed = True
def set_source(self, source):
for item in source:
# Error case handling for check mode
if (
self.module.check_mode
and item.startswith("ipset:")
and item.split(":")[1] not in self.fw.config().getIPSetNames()
):
self.module.warn(
"%s does not exist - ensure it is defined in a previous task before running play outside check mode"
% item
)
self.changed = True
elif self.state == "enabled":
if self.runtime and not self.fw.querySource(self.zone, item):
if not self.module.check_mode:
self.fw.addSource(self.zone, item)
self.changed = True
if self.permanent and not self.fw_settings.querySource(item):
if not self.module.check_mode:
self.fw_settings.addSource(item)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.querySource(self.zone, item):
if not self.module.check_mode:
self.fw.removeSource(self.zone, item)
self.changed = True
if self.permanent and self.fw_settings.querySource(item):
if not self.module.check_mode:
self.fw_settings.removeSource(item)
self.changed = True
def set_interface(self, interface):
for item in interface:
if self.state == "enabled":
if self.runtime and not self.fw.queryInterface(self.zone, item):
if not self.module.check_mode:
self.fw.changeZoneOfInterface(self.zone, item)
self.changed = True
self.set_interface_changed = True
if self.permanent:
nm_used, if_changed = try_set_zone_of_interface(
self.module, self.zone, item
)
if nm_used:
if if_changed:
self.changed = True
self.set_interface_changed = True
elif not self.fw_settings.queryInterface(item):
if not self.module.check_mode:
old_zone_name = self.fw.config().getZoneOfInterface(item)
if old_zone_name != self.zone:
if old_zone_name:
old_zone_obj = self.fw.config().getZoneByName(
old_zone_name
)
old_zone_settings = old_zone_obj.getSettings()
old_zone_settings.removeInterface(item)
old_zone_obj.update(old_zone_settings)
self.fw_settings.addInterface(item)
self.changed = True
self.set_interface_changed = True
elif self.state == "disabled":
if self.runtime and self.fw.queryInterface(self.zone, item):
if not self.module.check_mode:
self.fw.removeInterface(self.zone, item)
self.changed = True
self.set_interface_changed = True
if self.permanent:
nm_used, if_changed = try_set_zone_of_interface(
self.module, "", item
)
if nm_used:
if if_changed:
self.changed = True
self.set_interface_changed = True
elif self.fw_settings.queryInterface(item):
if not self.module.check_mode:
self.fw_settings.removeInterface(item)
self.changed = True
self.set_interface_changed = True
def set_icmp_block(self, icmp_block):
for item in icmp_block:
if self.state == "enabled":
if self.runtime and not self.fw.queryIcmpBlock(self.zone, item):
if not self.module.check_mode:
self.fw.addIcmpBlock(self.zone, item, self.timeout)
self.changed = True
if self.permanent and not self.fw_settings.queryIcmpBlock(item):
if not self.module.check_mode:
self.fw_settings.addIcmpBlock(item)
self.changed = True
elif self.state == "disabled":
if self.runtime and self.fw.queryIcmpBlock(self.zone, item):
if not self.module.check_mode:
self.fw.removeIcmpBlock(self.zone, item)
self.changed = True
if self.permanent and self.fw_settings.queryIcmpBlock(item):
if not self.module.check_mode:
self.fw_settings.removeIcmpBlock(item)
self.changed = True
def set_icmp_block_inversion(self, icmp_block_inversion):
if icmp_block_inversion:
if self.runtime and not self.fw.queryIcmpBlockInversion(self.zone):
if not self.module.check_mode:
self.fw.addIcmpBlockInversion(self.zone)
self.changed = True
if self.permanent and not self.fw_settings.queryIcmpBlockInversion():
if not self.module.check_mode:
self.fw_settings.addIcmpBlockInversion()
self.changed = True
else:
if self.runtime and self.fw.queryIcmpBlockInversion(self.zone):
if not self.module.check_mode:
self.fw.removeIcmpBlockInversion(self.zone)
self.changed = True
if self.permanent and self.fw_settings.queryIcmpBlockInversion():
if not self.module.check_mode:
self.fw_settings.removeIcmpBlockInversion()
self.changed = True
def set_target(self, target):
if self.state in ["enabled", "present"]:
if self.permanent and self.fw_settings.getTarget() != target:
if not self.module.check_mode:
self.fw_settings.setTarget(target)
self.need_reload = True
self.changed = True
elif self.state in ["absent", "disabled"]:
target = "default"
if self.permanent and self.fw_settings.getTarget() != target:
if not self.module.check_mode:
self.fw_settings.setTarget(target)
self.need_reload = True
self.changed = True
class InMemoryBackend:
"""Implement operations using in-memory configuration.
This backend reads the existing configuration into memory and applies
changes to the in-memory representation. It can return both the original
and modified configurations for comparison or deferred application.
"""
def __init__(
self,
module,
online,
start_empty=False,
):
self.module = module
self.online = online
self.state = None
self.permanent = None
self.runtime = None
self.zone = None
self.timeout = None
self.changed = False
self.set_interface_changed = False
# Load the current configuration
self.original_config = config_to_dict(module, detailed=True, online=online)
self.firewalld_conf = copy.deepcopy(
self.original_config.get("firewalld_conf", {})
)
self.working_config_runtime = {}
if start_empty:
# start with the built-in default settings
self.default_zone = self.original_config.get(
"fallback_default_zone", "public"
)
self.working_config_permanent = copy.deepcopy(
self.original_config["default"]
)
if self.online:
self.working_config_runtime = copy.deepcopy(
self.original_config["default"]
)
self._move_interfaces_to_default_zone(
self.original_config["custom_runtime_with_defaults"]
)
else:
# start with the current permanent and runtime settings
self.default_zone = self.original_config.get("default_zone", "public")
self.working_config_permanent = copy.deepcopy(
self.original_config["custom_permanent_with_defaults"]
)
if self.online:
self.working_config_runtime = copy.deepcopy(
self.original_config["custom_runtime_with_defaults"]
)
self.original_default_zone = self.default_zone
def check_zone_exists(self):
self.zone_exists = False
# Check in permanent config first
if self.permanent and self.zone in self.working_config_permanent.get(
"zones", {}
):
self.zone_exists = True
if (
self.online
and not self.zone_exists
and self.runtime
and self.zone in self.working_config_runtime.get("zones", {})
):
self.zone_exists = True
return self.zone_exists
def _get_zone_config(self, config_type="permanent"):
"""Get the configuration for the current zone.
Args:
config_type: Either "permanent" or "runtime"
"""
if config_type == "permanent":
config = self.working_config_permanent
elif self.online:
config = self.working_config_runtime
else:
return None
return config["zones"].get(self.zone, None)
def finalize(self):
"""No-op for in-memory backend."""
pass
def get_configs(self):
"""Return both original and working configurations.
Returns:
tuple: (original_config, working_config_permanent, working_config_runtime, default_zone, firewalld_conf)
"""
return (
self.original_config,
self.working_config_permanent,
self.working_config_runtime,
self.default_zone,
self.firewalld_conf,
)
def set_firewalld_conf(self, firewalld_conf, allow_zone_drifting_deprecated):
"""Set firewalld.conf options."""
# Store firewalld_conf settings in permanent config only
# (firewalld.conf is a permanent configuration file)
if (
not allow_zone_drifting_deprecated
and "allow_zone_drifting" in firewalld_conf
and firewalld_conf.get("allow_zone_drifting")
!= self.firewalld_conf.get("allow_zone_drifting")
):
self.firewalld_conf["allow_zone_drifting"] = firewalld_conf.get(
"allow_zone_drifting"
)
self.changed = True
def _new_zone(self):
if HAS_FIREWALLD:
return export_config_dict(Zone())
else:
return {}
def set_zone(self):
"""Create or remove a zone."""
# A zone must be present or absent in both permanent and runtime configurations
if (
self.state == "present"
and self.zone not in self.working_config_permanent["zones"]
):
self.working_config_permanent["zones"][self.zone] = self._new_zone()
if self.online:
self.working_config_runtime["zones"][self.zone] = self._new_zone()
self.changed = True
elif (
self.state == "absent"
and self.zone in self.working_config_permanent["zones"]
):
del self.working_config_permanent["zones"][self.zone]
if self.online:
del self.working_config_runtime["zones"][self.zone]
self.changed = True
# removing a zone online requires a reload - if you remove the default zone, it
# is set back to the original
if self.default_zone == self.zone:
self.default_zone = self.original_default_zone
self.zone = self.default_zone
def _move_interfaces_to_default_zone(self, working_config_runtime=None):
if self.online:
if not working_config_runtime:
src = self.working_config_runtime
dest = self.working_config_runtime
else:
src = working_config_runtime
dest = self.working_config_runtime
for zone_name, zone_config in src.get("zones", {}).items():
if (
src is not dest or zone_name != self.default_zone
) and "interfaces" in zone_config:
for interface in zone_config["interfaces"]:
if interface not in dest["zones"][self.default_zone].get(
"interfaces", []
):
dest["zones"][self.default_zone].setdefault(
"interfaces", []
).append(interface)
if src is dest:
del zone_config["interfaces"]
def set_default_zone(self, zone):
"""Set the default zone."""
# Default zone applies to both permanent and runtime configurations
if self.default_zone != zone:
self.default_zone = zone
self.changed = True
# move all of the runtime interfaces to the new default zone
self._move_interfaces_to_default_zone()
def get_default_zone(self):
"""Get the default zone."""
return self.default_zone
def _new_service(self):
if HAS_FIREWALLD:
return export_config_dict(Service())
else:
return {}
def set_service(
self,
service_operation,
service,
description,
short,
port,
protocol,
source_port,
helper_module,
destination_ipv4,
destination_ipv6,
includes,
):
"""Configure services."""
if service_operation and self.permanent:
working_configs = [self.working_config_permanent]
if self.online:
working_configs.append(self.working_config_runtime)
for working_config in working_configs:
if self.state == "present":
if "services" not in working_config:
working_config["services"] = {}
if service not in working_config["services"]:
working_config["services"][service] = self._new_service()
self.changed = True
svc = working_config["services"][service]
if (
description is not None
and svc.get("description") != description
):
svc["description"] = description
self.changed = True
if short is not None and svc.get("short") != short:
svc["short"] = short
self.changed = True
for port_tuple in port:
if port_tuple not in svc.get("ports", []):
svc.setdefault("ports", []).append(port_tuple)
self.changed = True
for _protocol in protocol:
if _protocol not in svc.get("protocols", []):
svc.setdefault("protocols", []).append(_protocol)
self.changed = True
for port_tuple in source_port:
if port_tuple not in svc.get("source_ports", []):
svc.setdefault("source_ports", []).append(port_tuple)
self.changed = True
for _module in helper_module:
if _module not in svc.get("helpers", []):
svc.setdefault("helpers", []).append(_module)
self.changed = True
if destination_ipv4:
if svc.get("destination", {}).get("ipv4") != destination_ipv4:
svc.setdefault("destination", {})["ipv4"] = destination_ipv4
self.changed = True
if destination_ipv6:
if svc.get("destination", {}).get("ipv6") != destination_ipv6:
svc.setdefault("destination", {})["ipv6"] = destination_ipv6
self.changed = True
for _include in includes:
if _include not in svc.get("includes", []):
svc.setdefault("includes", []).append(_include)
self.changed = True
elif self.state == "absent" and service in working_config.get(
"services", {}
):
if any(
(
port,
source_port,
protocol,
helper_module,
destination_ipv4,
destination_ipv6,
includes,
)
):
# Remove specific items
svc = working_config["services"][service]
for port_tuple in port:
if port_tuple in svc.get("ports", []):
svc["ports"].remove(port_tuple)
self.changed = True
if "ports" in svc and not svc["ports"]:
del svc["ports"]
for _protocol in protocol:
if _protocol in svc.get("protocols", []):
svc["protocols"].remove(_protocol)
self.changed = True
if "protocols" in svc and not svc["protocols"]:
del svc["protocols"]
for port_tuple in source_port:
if port_tuple in svc.get("source_ports", []):
svc["source_ports"].remove(port_tuple)
self.changed = True
if "source_ports" in svc and not svc["source_ports"]:
del svc["source_ports"]
for _module in helper_module:
if _module in svc.get("helpers", []):
svc["helpers"].remove(_module)
self.changed = True
if "helpers" in svc and not svc["helpers"]:
del svc["helpers"]
if (
destination_ipv4
and svc.get("destination", {}).get("ipv4")
== destination_ipv4
):
del svc["destination"]["ipv4"]
self.changed = True
if (
destination_ipv6
and svc.get("destination", {}).get("ipv6")
== destination_ipv6
):
del svc["destination"]["ipv6"]
self.changed = True
if "destination" in svc and not svc["destination"]:
del svc["destination"]
for _include in includes:
if _include in svc.get("includes", []):
svc["includes"].remove(_include)
self.changed = True
if "includes" in svc and not svc["includes"]:
del svc["includes"]
else:
# Remove entire service
del working_config["services"][service]
self.changed = True
else:
# Zone service operation - applies to both permanent and runtime
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for item in service:
service_exists = item in self.working_config_permanent.get(
"services", {}
) or (
self.online
and item in self.working_config_runtime.get("services", {})
)
if service_exists and self.state == "enabled":
if item not in zone_config.get("services", []):
zone_config.setdefault("services", []).append(item)
self.changed = True
elif service_exists and self.state == "disabled":
if item in zone_config.get("services", []):
zone_config["services"].remove(item)
self.changed = True
else:
if self.module.check_mode:
self.module.warn(
"Service does not exist - "
+ item
+ ". Ensure that you define the service in the playbook before running it in diff mode"
)
else:
self.module.fail_json(msg="INVALID SERVICE - " + item)
def _new_ipset(self, ipset_type):
new_ipset = {}
if HAS_FIREWALLD:
new_ipset = export_config_dict(IPSet())
new_ipset["type"] = ipset_type
return new_ipset
def set_ipset(
self, ipset, description, short, ipset_type, ipset_entries, ipset_options
):
"""Configure ipsets (permanent only)."""
ipset_options = check_and_normalize_ipset(
self.module, ipset, ipset_entries, ipset_options
)
existing_ipset = self.working_config_permanent.get("ipsets", {}).get(
ipset,
{},
)
if existing_ipset:
if ipset_type and ipset_type != existing_ipset.get("type"):
self.module.fail_json(
msg="Name conflict when creating ipset - "
"ipset %s of type %s already exists"
% (ipset, existing_ipset.get("type"))
)
# set ipset in permanent, and runtime if set
for config_type, working_config in (
(self.permanent, self.working_config_permanent),
(self.online and self.runtime, self.working_config_runtime),
):
if config_type and working_config is not None:
if self.state == "present":
if not existing_ipset:
if "ipsets" not in working_config:
working_config["ipsets"] = {}
working_config["ipsets"][ipset] = self._new_ipset(ipset_type)
self.changed = True
ipset_cfg = working_config["ipsets"][ipset]
if (
description is not None
and ipset_cfg.get("description") != description
):
ipset_cfg["description"] = description
self.changed = True
if short is not None and ipset_cfg.get("short") != short:
ipset_cfg["short"] = short
self.changed = True
for entry in ipset_entries:
if entry not in ipset_cfg.get("entries", []):
ipset_cfg.setdefault("entries", []).append(entry)
self.changed = True
for option, value in ipset_options.items():
if ipset_cfg.get("options", {}).get(option) != value:
ipset_cfg.setdefault("options", {})[option] = value
self.changed = True
elif self.state == "absent" and existing_ipset:
if ipset_entries or ipset_options:
# Remove specific entries/options
ipset_cfg = working_config["ipsets"][ipset]
for entry in ipset_entries:
if entry in ipset_cfg.get("entries", []):
ipset_cfg["entries"].remove(entry)
self.changed = True
else:
# Remove entire ipset
del working_config["ipsets"][ipset]
# if no more ipsets, remove the top level
if not working_config["ipsets"]:
del working_config["ipsets"]
self.changed = True
def _set_ports_or_source_ports(self, port, port_type):
"""Configure ports or source_ports in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for _port, _protocol in port:
# keep as tuple here
port_spec = (_port, _protocol)
if self.state == "enabled":
if port_spec not in zone_config.get(port_type, []):
zone_config.setdefault(port_type, []).append(port_spec)
self.changed = True
elif self.state == "disabled":
if port_spec in zone_config.get(port_type, []):
zone_config[port_type].remove(port_spec)
self.changed = True
def set_port(self, port):
"""Configure ports in a zone."""
self._set_ports_or_source_ports(port, "ports")
def set_source_port(self, source_port):
"""Configure source ports in a zone."""
self._set_ports_or_source_ports(source_port, "source_ports")
def set_forward_port(self, forward_port):
"""Configure port forwarding in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for forward_port_item in forward_port:
if self.state == "enabled":
if forward_port_item not in zone_config.get(
"forward_ports", []
):
zone_config.setdefault("forward_ports", []).append(
forward_port_item
)
self.changed = True
elif self.state == "disabled":
if forward_port_item in zone_config.get("forward_ports", []):
zone_config["forward_ports"].remove(forward_port_item)
self.changed = True
def set_masquerade(self, masquerade):
"""Configure masquerading in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
if zone_config.get("masquerade", False) != masquerade:
zone_config["masquerade"] = masquerade
self.changed = True
# note: rich_rule is a single string, normalized using str(Rich_Rule(rule_str=original_string))
# the zone config field 'rich_rule' is a list of normalized strings
def set_rich_rule(self, rich_rule):
"""Configure rich rules in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for item in rich_rule:
if self.state == "enabled":
if item not in zone_config.get("rich_rule", []):
zone_config.setdefault("rich_rule", []).append(item)
self.changed = True
elif self.state == "disabled":
if item in zone_config.get("rich_rule", []):
zone_config["rich_rule"].remove(item)
self.changed = True
def set_source(self, source):
"""Configure sources in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for item in source:
if self.state == "enabled":
if item not in zone_config.get("sources", []):
zone_config.setdefault("sources", []).append(item)
self.changed = True
elif self.state == "disabled":
if item in zone_config.get("sources", []):
zone_config["sources"].remove(item)
self.changed = True
def set_interface(self, interface):
"""Configure interfaces in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
# we have no way to know if this will actually change the firewall configuration,
# so set this flag here to let the real backend handle it even if there are
# no other changes.
self.set_interface_changed = True
for item in interface:
if self.state == "enabled":
if item not in zone_config.get("interfaces", []):
zone_config.setdefault("interfaces", []).append(item)
self.changed = True
elif self.state == "disabled":
if item in zone_config.get("interfaces", []):
zone_config["interfaces"].remove(item)
self.changed = True
def set_icmp_block(self, icmp_block):
"""Configure ICMP blocks in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
for item in icmp_block:
if self.state == "enabled":
if item not in zone_config.get("icmp_blocks", []):
zone_config.setdefault("icmp_blocks", []).append(item)
self.changed = True
elif self.state == "disabled":
if item in zone_config.get("icmp_blocks", []):
zone_config["icmp_blocks"].remove(item)
self.changed = True
def set_icmp_block_inversion(self, icmp_block_inversion):
"""Configure ICMP block inversion in a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
if (
zone_config.get("icmp_block_inversion", False)
!= icmp_block_inversion
):
zone_config["icmp_block_inversion"] = icmp_block_inversion
self.changed = True
def set_target(self, target):
"""Configure the target for a zone."""
for config_type, zone_config in (
(self.permanent, self._get_zone_config("permanent")),
(self.online and self.runtime, self._get_zone_config("runtime")),
):
if config_type and zone_config is not None:
if self.state in ["enabled", "present"]:
if zone_config.get("target", "default") != target:
zone_config["target"] = target
self.changed = True
elif self.state in ["absent", "disabled"]:
if zone_config.get("target", "default") != "default":
zone_config["target"] = "default"
self.changed = True
class OfflineCLIBackend:
"""Implement operations with firewall-offline-cmd.
This works during container builds and similar environments.
"""
def __init__(self, module, permanent, runtime, zone, state, timeout):
self.module = module
self.state = state
self.timeout = timeout
self.set_interface_changed = False
self.changed = False
if not permanent or runtime:
module.fail_json(
msg="runtime mode is not supported in offline environments"
)
# Get zone to operate on
if zone is None:
self.zone = self.cmd("--get-default-zone")
self.zone_exists = True
else:
self.zone = zone
zones = self.cmd("--get-zones").split()
self.zone_exists = zone in zones
def check_zone_exists(self):
return self.zone_exists
def _call_offline_cmd(self, args, check_rc=True):
argv = ["firewall-offline-cmd"] + list(args)
rc, out, err = self.module.run_command(argv, check_rc=check_rc)
out = out.strip()
self.module.debug("OfflineCLIBackend: %r -> exit %i, out: %s" % (argv, rc, out))
return (rc, out)
def cmd(self, *args):
"""Call firewall-offline-cmd with given arguments, expecting success."""
return self._call_offline_cmd(args)[1]
def change(self, *args):
"""Like cmd(), but skipped in check_mode.
Also set self.changed.
"""
if not self.module.check_mode:
self.cmd(*args)
self.changed = True
def query(self, *args):
"""Call firewall-offline-cmd query command, convert exit code to bool."""
rc = self._call_offline_cmd(args, check_rc=False)[0]
return True if rc == 0 else False
def finalize(self):
# nothing to do here, all changes are written immediately in offline mode
pass
def check_state(self, allowed, option):
"""Check and interpret self.state
allowed is a list of allowed values. E.g. most operations only accept
enabled/disabled, while others only accept present/absent, some accept
either. This keeps the behaviour bug-for-bug compatible with
OnlineAPIBackend.
Return True for enabled/present or False for disabled/absent.
"""
if self.state not in allowed:
self.module.fail_json(
msg="state '%s' not accepted for option '%s'" % (self.state, option)
)
return self.state in ["enabled", "present"]
def set_firewalld_conf(self, firewalld_conf, allow_zone_drifting_deprecated):
if allow_zone_drifting_deprecated:
# compatibility with OnlineAPIBackend: allow_zone_drifting gets
# ignored when deprecated, without failing the role
other_keys = set(firewalld_conf.keys()) - set(["allow_zone_drifting"])
if len(other_keys) == 0:
# parser in main() already wrote a warning
return
# there are currently no other supported options in firewalld_conf, so
# this should not happen; if it ever does, implement it
self.module.fail_json(
msg="firewalld_conf is not currently supported in offline mode; please file a bug"
)
def set_zone(self):
create = self.check_state(["present", "absent"], "zone")
if create != self.zone_exists:
self.change(
"--%s-zone=%s" % ("new" if create else "delete", self.zone),
)
if not create and self.zone_exists:
self.zone = None
self.zone_exists = False
def set_default_zone(self, zone):
if self.cmd("--get-default-zone") != zone:
self.change("--set-default-zone", zone)
def set_service(
self,
service_operation,
service,
description,
short,
port,
protocol,
source_port,
helper_module,
destination_ipv4,
destination_ipv6,
includes,
):
if service_operation:
present = self.check_state(["present", "absent"], "service")
known_services = self.cmd("--get-services").split()
service_exists = service in known_services
if present:
if not service_exists:
self.change("--new-service", service)
existing_description = self.cmd(
"--service", service, "--get-description"
)
if description is not None and description != existing_description:
self.change("--service", service, "--set-description", description)
existing_short = self.cmd("--service", service, "--get-short")
if short is not None and short != existing_short:
self.change("--service", service, "--set-short", short)
for _port, _protocol in port:
spec = "%s/%s" % (_port, _protocol)
if not self.query("--service", service, "--query-port=" + spec):
self.change("--service", service, "--add-port=" + spec)
for _protocol in protocol:
if not self.query(
"--service", service, "--query-protocol=" + _protocol
):
self.change("--service", service, "--add-protocol=" + _protocol)
for _port, _protocol in source_port:
spec = "%s/%s" % (_port, _protocol)
if not self.query(
"--service", service, "--query-source-port=" + spec
):
self.change("--service", service, "--add-source-port=" + spec)
for _module in helper_module:
if not self.query(
"--service", service, "--query-helper=" + _module
):
self.change("--service", service, "--add-helper=" + _module)
if destination_ipv4 and not self.query(
"--service",
service,
"--query-destination=ipv4:" + destination_ipv4,
):
self.change(
"--service",
service,
"--set-destination=ipv4:" + destination_ipv4,
)
if destination_ipv6 and not self.query(
"--service",
service,
"--query-destination=ipv6:" + destination_ipv6,
):
self.change(
"--service",
service,
"--set-destination=ipv6:" + destination_ipv6,
)
for _include in includes:
if not self.query(
"--service", service, "--query-include=" + _include
):
self.change("--service", service, "--add-include=" + _include)
if not present and service_exists:
if port:
for _port, _protocol in port:
spec = "%s/%s" % (_port, _protocol)
if self.query("--service", service, "--query-port=" + spec):
self.change("--service", service, "--remove-port=" + spec)
if source_port:
for _port, _protocol in source_port:
spec = "%s/%s" % (_port, _protocol)
if self.query(
"--service", service, "--query-source-port=" + spec
):
self.change(
"--service", service, "--remove-source-port=" + spec
)
if protocol:
for _protocol in protocol:
if self.query(
"--service", service, "--query-protocol=" + _protocol
):
self.change(
"--service", service, "--remove-protocol=" + _protocol
)
if helper_module:
for _module in helper_module:
if self.query(
"--service", service, "--query-helper=" + _module
):
self.change(
"--service", service, "--remove-helper=" + _module
)
if destination_ipv4 and self.query(
"--service",
service,
"--query-destination=ipv4:" + destination_ipv4,
):
# asymmetric, but correct: no IP value here, just the protocol version
self.change("--service", service, "--remove-destination=ipv4")
if destination_ipv6 and self.query(
"--service",
service,
"--query-destination=ipv6:" + destination_ipv6,
):
self.change("--service", service, "--remove-destination=ipv6")
for _include in includes:
if self.query("--service", service, "--query-include=" + _include):
self.change(
"--service", service, "--remove-include=" + _include
)
if not any(
(
port,
source_port,
protocol,
helper_module,
destination_ipv4,
destination_ipv6,
)
):
self.change("--delete-service", service)
# not service_operation: add/remove service from zone
else:
known_services = self.cmd("--get-services").split()
enable = self.check_state(["enabled", "disabled"], "service")
for item in service:
if item not in known_services:
if self.module.check_mode:
self.module.warn(
"Service does not exist - "
+ item
+ ". Ensure that you define the service in the playbook before running it in diff mode"
)
continue
else:
self.module.fail_json(msg="INVALID SERVICE - " + item)
cur = self.query("--zone", self.zone, "--query-service=" + item)
if cur != enable:
op = "--add-service=" if enable else "--remove-service-from-zone="
self.change("--zone", self.zone, op + item)
def set_ipset(
self, ipset, description, short, ipset_type, ipset_entries, ipset_options
):
addr_type = get_ipset_entries_type(ipset_entries, self.module)
if addr_type is None and ipset_entries:
self.module.fail_json(
msg="ipset %s: Invalid IP address - %s" % (ipset, str(ipset_entries))
)
normalize_ipset_options(ipset_options)
ipset_options_list = []
if ipset_options:
for kk, vv in ipset_options.items():
ipset_options_list.append("--option")
ipset_options_list.append(kk + "=" + str(vv))
if addr_type == "ipv6" and "family=inet6" not in ipset_options_list:
ipset_options_list.append("--option")
ipset_options_list.append("family=inet6")
if addr_type == "ipv4" and "family=inet6" in ipset_options_list:
self.module.fail_json(
msg="ipset %s: family=inet6 is not supported for IPv4 ipset_entries %s"
% (ipset, ", ".join(ipset_entries))
)
if addr_type == "ipv6" and "family=inet" in ipset_options_list:
self.module.fail_json(
msg="ipset %s: family=inet is not supported for IPv6 ipset_entries %s"
% (ipset, ", ".join(ipset_entries))
)
present = self.check_state(["present", "absent"], "ipset")
known_ipsets = self.cmd("--get-ipsets").split()
ipset_exists = ipset in known_ipsets
if ipset_exists and ipset_type:
m = re.search(r"\stype: (.*)$", self.cmd("--info-ipset", ipset), re.M)
if not m:
self.module.fail_json(
"'firewall-offline-cmd --info-ipset %s' did not print 'type:'"
% ipset
)
existing_type = m.group(1)
if ipset_type != existing_type:
self.module.fail_json(
msg="Name conflict when creating ipset - "
"ipset %s of type %s already exists" % (ipset, existing_type)
)
if present:
if not ipset_exists:
if not ipset_type:
self.module.fail_json(
msg="ipset %s: ipset_type needed when creating a new ipset"
% ipset
)
self.change(
"--new-ipset", ipset, "--type=%s" % ipset_type, *ipset_options_list
)
existing_description = self.cmd("--ipset", ipset, "--get-description")
if description is not None and description != existing_description:
self.change("--ipset", ipset, "--set-description", description)
existing_short = self.cmd("--ipset", ipset, "--get-short")
if short is not None and short != existing_short:
self.change("--ipset", ipset, "--set-short", short)
for entry in ipset_entries:
if not self.query("--ipset", ipset, "--query-entry", entry):
self.change("--ipset", ipset, "--add-entry", entry)
# remove
elif ipset_exists:
if ipset_entries:
for entry in ipset_entries:
if self.query("--ipset", ipset, "--query-entry", entry):
self.change("--ipset", ipset, "--remove-entry", entry)
else:
rc, bound_zone = self._call_offline_cmd(
["--get-zone-of-source=ipset:" + ipset], check_rc=False
)
if rc == 0:
if self.module.check_mode:
self.module.warn(
"Ensure ipset:%s is removed from zone %s before attempting to remove it"
% (ipset, bound_zone)
)
else:
self.module.fail_json(
msg="Remove ipset:%s from all zones before attempting to remove it"
% ipset
)
self.change("--delete-ipset", ipset)
def set_port(self, port):
enable = self.check_state(["enabled", "disabled"], "port")
for _port, _protocol in port:
spec = "%s/%s" % (_port, _protocol)
cur = self.query("--zone", self.zone, "--query-port=" + spec)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-port=%s" % ("add" if enable else "remove", spec),
)
def set_source_port(self, source_port):
enable = self.check_state(["enabled", "disabled"], "source_port")
for _port, _protocol in source_port:
spec = "%s/%s" % (_port, _protocol)
cur = self.query("--zone", self.zone, "--query-source-port=" + spec)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-source-port=%s" % ("add" if enable else "remove", spec),
)
def set_forward_port(self, forward_port):
enable = self.check_state(["enabled", "disabled"], "forward_port")
for _port, _protocol, _to_port, _to_addr in forward_port:
spec = "port=%s:proto=%s" % (_port, _protocol)
if _to_port is not None:
spec += ":toport=%s" % _to_port
if _to_addr is not None:
spec += ":toaddr=%s" % _to_addr
cur = self.query("--zone", self.zone, "--query-forward-port=" + spec)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-forward-port=%s" % ("add" if enable else "remove", spec),
)
def set_masquerade(self, masquerade):
cur = self.query("--zone", self.zone, "--query-masquerade")
if cur != masquerade:
self.change(
"--zone",
self.zone,
"--%s-masquerade" % ("add" if masquerade else "remove"),
)
def set_rich_rule(self, rich_rule):
enable = self.check_state(["enabled", "disabled"], "rich_rule")
for item in rich_rule:
# note: item is a string, normalized using str(Rich_Rule(rule_str=original_string))
cur = self.query("--zone", self.zone, "--query-rich-rule=" + item)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-rich-rule=%s" % ("add" if enable else "remove", item),
)
def set_source(self, source):
if self.module.check_mode:
ipset_names = self.cmd("--get-ipsets").split()
enable = self.check_state(["enabled", "disabled"], "source")
for item in source:
# Error case handling for check mode
if (
self.module.check_mode
and item.startswith("ipset:")
and item.split(":")[1] not in ipset_names
):
self.module.warn(
"%s does not exist - ensure it is defined in a previous task before running play outside check mode"
% item
)
self.changed = True
else:
cur = self.query("--zone", self.zone, "--query-source=" + item)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-source=%s" % ("add" if enable else "remove", item),
)
def set_interface(self, interface):
# we can't do this via NM like in OnlineAPIBackend, always go via firewalld config
enable = self.check_state(["enabled", "disabled"], "interface")
for item in interface:
cur = self.query("--zone", self.zone, "--query-interface=" + item)
if cur != enable:
self.change(
"--zone",
self.zone,
# note: --change-interface first removes it from the old zone
"--%s-interface=%s" % ("change" if enable else "remove", item),
)
self.set_interface_changed = True
def set_icmp_block(self, icmp_block):
enable = self.check_state(["enabled", "disabled"], "icmp_block")
for item in icmp_block:
cur = self.query("--zone", self.zone, "--query-icmp-block=" + item)
if cur != enable:
self.change(
"--zone",
self.zone,
"--%s-icmp-block=%s" % ("add" if enable else "remove", item),
)
def set_icmp_block_inversion(self, icmp_block_inversion):
cur = self.query("--zone", self.zone, "--query-icmp-block-inversion")
if cur != icmp_block_inversion:
self.change(
"--zone",
self.zone,
"--%s-icmp-block-inversion"
% ("add" if icmp_block_inversion else "remove"),
)
def set_target(self, target):
enable = self.check_state(
["enabled", "present", "disabled", "absent"], "target"
)
cur_target = self.cmd("--zone", self.zone, "--get-target")
new_target = target if enable else "default"
if new_target != cur_target:
self.change("--zone", self.zone, "--set-target", new_target)
PCI_REGEX = re.compile("[0-9a-fA-F]{4}:[0-9a-fA-F]{4}")
# NOTE: Because of PEP632, we cannot use distutils.
# In addition, because of the wide range of python
# versions we have to support, there isn't a good
# version parser across all of them, that is provided
# with Ansible.
def lsr_parse_version(v_str):
v_ary = v_str.split(".")
v = []
for v_ary_str in v_ary:
try:
v.append(int(v_ary_str))
except ValueError:
v.append(0)
return v
pci_ids = None
def get_interface_pci():
pci_dict = {}
for interface in nm_get_interfaces():
# udi/device/[vendor, device]
interface_ids = []
device_udi = nm_get_client().get_device_by_iface(interface).get_udi()
device_path = os.path.join(device_udi, "device")
for field in ["vendor", "device"]:
try:
with open(os.path.join(device_path, field)) as _file:
interface_ids.append(_file.readline().strip(" \n")[2:])
except IOError:
continue
if len(interface_ids) != 2:
continue
interface_ids = ":".join(interface_ids)
if interface_ids not in pci_dict:
pci_dict[interface_ids] = [interface]
else:
pci_dict[interface_ids].append(interface)
return pci_dict
def parse_pci_id(module, item):
if not module.params["online"]:
module.fail_json(msg="interface_pci_id is not supported in offline mode.")
warning = ""
if PCI_REGEX.search(item):
if not NM_IMPORTED:
module.fail_json(
msg="interface_pci_id is only supported with NetworkManager. If you want to use this feature, please install NetworkManager."
)
global pci_ids
if not pci_ids:
pci_ids = get_interface_pci()
interface_names = pci_ids.get(item)
if interface_names:
return interface_names, warning
warning = "No network interfaces found with PCI device ID %s" % item
if callable(getattr(module, "warn", None)):
module.warn(warning)
warning = ""
else:
module.fail_json(
msg="PCI id %s does not match format: XXXX:XXXX (X = hexadecimal number)"
% item
)
return [], warning
def parse_port(module, item):
_port, _protocol = item.split("/")
if _protocol is None:
module.fail_json(msg="improper port format (missing protocol?)")
return (_port, _protocol)
ipv4_charset = "0123456789./"
ipv6_charset = "0123456789abcdef:/"
def parse_destination_address(module, item):
# Preventing long iterations for no reason
if len(item) > 43:
module.fail_json(msg="destination argument too long to be valid")
ipv4 = True
ipv6 = True
for character in item:
if character not in ipv4_charset:
ipv4 = False
if character not in ipv6_charset:
ipv6 = False
if (ipv4 and ipv6) or (not ipv4 and not ipv6):
module.fail_json(msg="Invalid IPv4 or IPv6 address - " + item)
# ipv4 specific error checking
if ipv4:
address = item.split(".")
if len(address) != 4:
module.fail_json(msg="IPv4 address does not have 4 octets - " + item)
for octet in range(4):
if "/" in address[octet] and octet != 3:
module.fail_json(
msg="IPv4 address can only have a / "
"at the end of the address to specify "
"a subnet mask"
)
octet_value = address[octet]
if octet == 3 and "/" in octet_value:
octet_value, mask = octet_value.split("/")
if int(mask) > 32:
module.fail_json(
msg="invalid IPv4 subnet mask - "
+ mask
+ " (must be between 0 and 32 inclusive)"
)
if int(octet_value) > 255:
module.fail_json(
msg="invalid IPv4 octet "
+ str(octet_value)
+ " in address "
+ item
)
return "ipv4"
if ipv6:
address = item.split(":")
num_segments = len(address)
if num_segments > 10:
module.fail_json(
msg="Invalid IPv6 address " + item + " - too many segments"
)
for segment_number in range(num_segments):
segment = address[segment_number]
if segment != "":
if segment_number != num_segments - 1 and "/" in segment:
module.fail_json(
msg="Invalid IPv6 address - subnet mask"
" found before last segment"
)
if segment_number == num_segments - 1 and "/" in segment:
segment, mask = segment.split("/")
if int(mask) > 128:
module.fail_json(
msg="Invalid IPv6 address "
+ item
+ " - subnet mask "
+ mask
+ " invalid"
)
if int(segment, 16) > 65535:
module.fail_json(
msg="Invalid IPv6 address " + item + " -"
" invalid segment " + segment
)
return "ipv6"
def parse_helper_module(module, item):
item = item.split("_")
_module = [word for word in item if word != "nf" or word != "conntrack"]
return "_".join(_module)
def get_forward_port(module):
forward_port = module.params["forward_port"]
if isinstance(forward_port, list):
return forward_port
else:
return [forward_port]
def parse_forward_port(module, item):
type_string = "forward_port"
_port = None
_protocol = None
_to_port = None
_to_addr = None
if isinstance(item, dict):
if "port" not in item:
module.fail_json(
msg="%s is missing field 'port' in %s" % (type_string, item)
)
else:
_port = str(item["port"])
if "proto" not in item:
module.fail_json(
msg="%s is missing field 'proto' in %s" % (type_string, item)
)
else:
_protocol = item["proto"]
if "toport" in item:
_to_port = str(item["toport"])
else:
_to_port = None
_to_addr = item.get("toaddr")
elif isinstance(item, lsr_string_types):
args = item.split(";")
if len(args) == 3:
__port, _to_port, _to_addr = args
else:
module.fail_json(msg="improper %s format: %s" % (type_string, item))
_port, _protocol = __port.split("/")
if _protocol is None:
module.fail_json(msg="improper %s format (missing protocol?)" % type_string)
if _to_port == "":
_to_port = None
if _to_addr == "":
_to_addr = None
else:
module.fail_json(
msg="improper %s type (must be str or dict): %s" % (type_string, item)
)
return (_port, _protocol, _to_port, _to_addr)
def check_allow_zone_drifting(firewalld_conf):
if isinstance(firewalld_conf["allow_zone_drifting"], bool):
if firewalld_conf["allow_zone_drifting"]:
firewalld_conf["allow_zone_drifting"] = "yes"
else:
firewalld_conf["allow_zone_drifting"] = "no"
# Parse all suboptions of firewalld_conf into how they will be used by the role
# Return True if all suboptions were emptied as a result
def check_firewalld_conf(firewalld_conf):
check_allow_zone_drifting(firewalld_conf)
def get_base_argument_spec():
"""Return the base argument spec for firewall configuration parameters."""
return dict(
firewalld_conf=dict(
required=False,
type="dict",
options=dict(
allow_zone_drifting=dict(required=False, type="bool", default=None),
),
default=None,
),
service=dict(required=False, type="list", elements="str", default=[]),
port=dict(required=False, type="list", elements="str", default=[]),
source_port=dict(required=False, type="list", elements="str", default=[]),
forward_port=dict(
required=False,
type="raw",
default=[],
aliases=["port_forward"],
deprecated_aliases=[
{
"name": "port_forward",
"date": "2021-09-23",
"collection_name": "ansible.posix",
},
],
),
masquerade=dict(required=False, type="bool", default=None),
rich_rule=dict(required=False, type="list", elements="str", default=[]),
source=dict(required=False, type="list", elements="str", default=[]),
interface=dict(required=False, type="list", elements="str", default=[]),
interface_pci_id=dict(required=False, type="list", elements="str", default=[]),
icmp_block=dict(required=False, type="list", elements="str", default=[]),
icmp_block_inversion=dict(required=False, type="bool", default=None),
timeout=dict(required=False, type="int", default=0),
target=dict(
required=False,
type="str",
choices=["default", "ACCEPT", "DROP", "%%REJECT%%"],
default=None,
),
zone=dict(required=False, type="str", default=None),
set_default_zone=dict(required=False, type="str", default=None),
ipset=dict(required=False, type="str", default=None),
ipset_type=dict(required=False, type="str", default=None),
ipset_entries=dict(required=False, type="list", elements="str", default=[]),
ipset_options=dict(required=False, type="dict", default={}),
permanent=dict(required=False, type="bool", default=None),
runtime=dict(
required=False,
type="bool",
default=None,
aliases=["immediate"],
deprecated_aliases=[
{
"name": "immediate",
"date": "2021-09-23",
"collection_name": "ansible.posix",
},
],
),
state=dict(
choices=["enabled", "disabled", "present", "absent"],
required=False,
default=None,
),
description=dict(required=False, type="str", default=None),
short=dict(required=False, type="str", default=None),
protocol=dict(required=False, type="list", elements="str", default=[]),
helper_module=dict(required=False, type="list", elements="str", default=[]),
destination=dict(required=False, type="list", elements="str", default=[]),
includes=dict(required=False, type="list", elements="str", default=[]),
previous=dict(required=False, choices=["replaced", "kept"], default="kept"),
)
def get_full_argument_spec():
full_spec = get_base_argument_spec()
full_spec.update(
dict(
online=dict(required=False, type="bool", default=True),
__called_from_role=dict(required=False, type="bool", default=False),
)
)
return full_spec
def process_single_config(
module,
warnings,
config_params=None,
backend=None,
online_param=None,
__called_from_role_param=None,
):
"""
Process a single configuration, either from module.params or from a config dict.
Args:
module: The Ansible module object
config_params: Optional config dict to use instead of module.params
backend: Optional backend object to use instead of creating a new one
Returns a tuple of (backend, changed) or None if no action needed.
"""
# Use config_params if provided, otherwise use module.params
if config_params is None:
params = module.params
else:
# Merge config_params with defaults from base argument spec
params = {}
base_spec = get_base_argument_spec()
for key, spec in base_spec.items():
params[key] = config_params.get(key, spec.get("default"))
# Argument parse
firewalld_conf = params["firewalld_conf"]
if firewalld_conf:
check_firewalld_conf(firewalld_conf)
allow_zone_drifting_deprecated = lsr_parse_version(
FW_VERSION
) >= lsr_parse_version("1.0.0")
if allow_zone_drifting_deprecated and firewalld_conf.get("allow_zone_drifting"):
if callable(getattr(module, "warn", None)):
module.warn(
"AllowZoneDrifting is deprecated in this version of firewalld and no longer supported"
)
else:
warnings.append(
"AllowZoneDrifting is deprecated in this version of firewalld and no longer supported"
)
else:
# CodeQL will produce an error without this line
allow_zone_drifting_deprecated = None
service = params["service"]
short = params["short"]
description = params["description"]
protocol = params["protocol"]
helper_module = []
for _module in params["helper_module"]:
helper_module.append(parse_helper_module(module, _module))
port = []
for port_proto in params["port"]:
port.append(parse_port(module, port_proto))
source_port = []
for port_proto in params["source_port"]:
source_port.append(parse_port(module, port_proto))
forward_port = []
# Simulate get_forward_port for config_params
if config_params is None:
forward_port_items = get_forward_port(module)
else:
forward_port_raw = params["forward_port"]
if isinstance(forward_port_raw, list):
forward_port_items = forward_port_raw
else:
forward_port_items = [forward_port_raw] if forward_port_raw else []
for item in forward_port_items:
forward_port.append(parse_forward_port(module, item))
masquerade = params["masquerade"]
rich_rule = []
for item in params["rich_rule"]:
try:
rule = str(Rich_Rule(rule_str=item))
rich_rule.append(rule)
except Exception as e:
module.fail_json(msg="Rich Rule '%s' is not valid: %s" % (item, str(e)))
source = params["source"]
destination_ipv4 = None
destination_ipv6 = None
for address in params["destination"]:
ip_type = parse_destination_address(module, address)
if ip_type == "ipv4" and not destination_ipv4:
destination_ipv4 = address
elif destination_ipv4 and ip_type == "ipv4":
module.fail_json(msg="cannot have more than one destination ipv4")
if ip_type == "ipv6" and not destination_ipv6:
destination_ipv6 = address
elif destination_ipv6 and ip_type == "ipv6":
module.fail_json(msg="cannot have more than one destination ipv6")
interface = params["interface"]
for _interface in params["interface_pci_id"]:
interface_names, warning = parse_pci_id(module, _interface)
for interface_name in interface_names:
if interface_name not in interface:
interface.append(interface_name)
if warning:
warnings.append(warning)
icmp_block = params["icmp_block"]
icmp_block_inversion = params["icmp_block_inversion"]
timeout = params["timeout"]
target = params["target"]
zone = params["zone"]
set_default_zone = params["set_default_zone"]
ipset = params["ipset"]
ipset_type = params["ipset_type"]
ipset_entries = params["ipset_entries"]
ipset_options = params["ipset_options"]
permanent = params["permanent"]
runtime = params["runtime"]
state = params["state"]
includes = params["includes"]
if online_param is None:
online = params["online"]
else:
online = online_param
if __called_from_role_param is None:
__called_from_role = params["__called_from_role"]
else:
__called_from_role = __called_from_role_param
# All options that require state to be set
state_required = any(
(
interface,
source,
service,
ipset,
source_port,
port,
forward_port,
icmp_block,
rich_rule,
)
)
# NOTE: The old implementation of this role would always set permanent to True if it was not set,
# and would set runtime to True if it was not set when called from the role.
# so replicate that behavior here in order to maintain backwards compatibility in the module
# in case someone is erroneously using the module directly
if __called_from_role:
if permanent is None:
permanent = True
if runtime is None:
runtime = online
else:
if permanent is None:
runtime = True
if not any((permanent, runtime)):
module.fail_json(msg="One of permanent, runtime needs to be enabled")
if (
masquerade is None
and icmp_block_inversion is None
and target is None
and zone is None
and not any(
(
service,
port,
source_port,
forward_port,
rich_rule,
source,
interface,
icmp_block,
set_default_zone,
firewalld_conf,
ipset,
)
)
):
# Skip this config if no actionable parameters are set
if config_params is not None:
return None
module.fail_json(
msg="One of service, port, source_port, forward_port, "
"masquerade, rich_rule, source, interface, icmp_block, "
"icmp_block_inversion, target, zone, set_default_zone, "
"ipset or firewalld_conf needs to be set"
)
# Checking for any permanent configuration operations
zone_operation = False
service_operation = False
ipset_operation = False
if state == "present" or state == "absent":
if (
masquerade is not None
and icmp_block_inversion is not None
and any(
(
forward_port,
rich_rule,
source,
interface,
icmp_block,
)
)
):
module.fail_json(
msg="states present and absent only usable for zone, service, or target operations "
"(when no parameters but zone or target and state(absent/present) are set, "
"or when state and service are set with optional parameters short, description "
" port, source_port, protocol, destination, or helper_module)"
)
# Zone, service, and ipset are incompatible with one another when state is set to present or absent
num_conflicting_args = len([x for x in [zone, ipset, service] if x])
if num_conflicting_args > 1:
module.fail_json(
msg="%s of {zone, service, ipset} while state present/absent, expected 1"
% num_conflicting_args
)
del num_conflicting_args
# While short and description are options for new zones, they are unimplemented
if target is None and zone is not None:
if any(
(
service,
description,
short,
port,
source_port,
helper_module,
protocol,
destination_ipv4,
destination_ipv6,
ipset_entries,
ipset_type,
ipset_options,
)
):
module.fail_json(
msg="short, description, port, source_port, helper_module, "
"protocol, destination, ipset_type, ipset_entries, or ipset_options cannot be set "
"while zone is specified "
"and state is set to present or absent"
)
else:
zone_operation = True
elif state == "absent" and any(
(
short,
description,
ipset_type,
)
):
module.fail_json(
msg="short, description and ipset_type can only be used when "
"state is present"
)
elif service:
if target is not None:
module.fail_json(
msg="Both service and target cannot be set "
"while state is either present or absent"
)
elif not permanent:
module.fail_json(
msg="permanent must be enabled for service configuration. "
"Additionally, service runtime configuration is not possible"
)
elif ipset_entries or ipset_type or ipset_options:
module.fail_json(
msg="ipset parameters cannot be set when configuring services"
)
else:
service_operation = True
elif ipset:
if target is not None:
module.fail_json(msg="Only one of {ipset, target} can be set")
elif not permanent:
module.fail_json(
msg="permanent must be enabled for ipset configuration"
)
else:
ipset_operation = True
if service_operation:
if len(service) != 1:
module.fail_json(
msg="can only add, modify, or remove one service at a time"
)
else:
service = service[0]
# firewalld.conf checks
if firewalld_conf and not permanent:
module.fail_json(msg="firewalld_conf can only be used with permanent")
# Parameter checks
if state == "disabled":
if timeout > 0:
module.fail_json(msg="timeout can not be used with state: disabled")
if masquerade:
module.fail_json(msg="masquerade can not be used with state: disabled")
if icmp_block_inversion:
module.fail_json(
msg="icmp_block_inversion can not be used with state: disabled"
)
# if target is not None:
# module.fail_json(
# msg="target can not be used with state: disabled"
# )
if timeout > 0:
_timeout_ok = any(
(
masquerade,
service,
port,
source_port,
forward_port,
rich_rule,
icmp_block,
)
)
if icmp_block_inversion is not None and not _timeout_ok:
module.fail_json(
msg="timeout can not be used with icmp_block_inversion only"
)
if len(source) > 0 and not _timeout_ok:
module.fail_json(msg="timeout can not be used with source only")
if len(interface) > 0 and not _timeout_ok:
module.fail_json(msg="timeout can not be used with interface only")
if target is not None and not _timeout_ok:
module.fail_json(msg="timeout can not be used with target only")
if len(source) > 0 and permanent is None:
module.fail_json(msg="source cannot be set without permanent")
if state is None and state_required:
module.fail_json(msg="Options invalid without state option set")
if not HAS_FIREWALLD:
module.fail_json(msg="No firewalld")
# Pre-run version checking
if lsr_parse_version(FW_VERSION) < lsr_parse_version("0.2.11"):
module.fail_json(
msg="Unsupported firewalld version %s, requires >= 0.2.11" % FW_VERSION
)
# Use provided backend or create a new one
if backend is None:
backendClass = OnlineAPIBackend if online else OfflineCLIBackend
backend = backendClass(module, permanent, runtime, zone, state, timeout)
else:
# Update backend state for this config
backend.state = state
backend.permanent = permanent
backend.runtime = runtime
backend.zone = zone or backend.get_default_zone()
backend.timeout = timeout
# error out if the zone does not exist and this is not a zone operation
if not zone_operation and not backend.check_zone_exists():
module.fail_json(msg="Zone '%s' does not exist." % backend.zone)
# Firewall modification starts here
if firewalld_conf:
backend.set_firewalld_conf(firewalld_conf, allow_zone_drifting_deprecated)
if zone_operation:
backend.set_zone()
if set_default_zone:
backend.set_default_zone(set_default_zone)
if service:
backend.set_service(
service_operation,
service,
description,
short,
port,
protocol,
source_port,
helper_module,
destination_ipv4,
destination_ipv6,
includes,
)
if ipset_operation:
backend.set_ipset(
ipset, description, short, ipset_type, ipset_entries, ipset_options
)
if port and not service_operation:
backend.set_port(port)
if source_port and not service_operation:
backend.set_source_port(source_port)
if forward_port:
backend.set_forward_port(forward_port)
if masquerade is not None:
backend.set_masquerade(masquerade)
if rich_rule:
backend.set_rich_rule(rich_rule)
if source:
backend.set_source(source)
if interface:
backend.set_interface(interface)
if icmp_block:
backend.set_icmp_block(icmp_block)
if icmp_block_inversion is not None:
backend.set_icmp_block_inversion(icmp_block_inversion)
if target is not None:
backend.set_target(target)
backend.finalize()
return backend.changed
def _recursive_diff_to_added_removed(recursive_result):
"""Normalize recursive_show_diffs() output into added/removed dicts."""
if not recursive_result:
return {"added": {}, "removed": {}}
removed = recursive_result.get("removed")
added = recursive_result.get("added")
if isinstance(removed, dict) and isinstance(added, dict):
return {
"added": copy.deepcopy(added),
"removed": copy.deepcopy(removed),
}
return {
"added": copy.deepcopy(added) if added is not None else {},
"removed": copy.deepcopy(removed) if removed is not None else {},
}
def get_diffs(
original_permanent,
original_runtime,
original_default_zone,
original_firewalld_conf,
current_permanent,
current_runtime,
current_default_zone,
current_firewalld_conf,
ignore_interface,
):
perm_raw = recursive_show_diffs(
original_permanent, current_permanent, None, ignore_interface=ignore_interface
)
diff = {"permanent": _recursive_diff_to_added_removed(perm_raw)}
if original_runtime and current_runtime:
run_raw = recursive_show_diffs(
original_runtime, current_runtime, None, ignore_interface=ignore_interface
)
else:
run_raw = None
diff["runtime"] = _recursive_diff_to_added_removed(run_raw)
if original_default_zone != current_default_zone:
diff["permanent"]["removed"]["default_zone"] = original_default_zone
diff["permanent"]["added"]["default_zone"] = current_default_zone
if (
original_firewalld_conf
and current_firewalld_conf
and original_firewalld_conf.get("allow_zone_drifting")
!= current_firewalld_conf.get("allow_zone_drifting")
):
diff["permanent"]["removed"]["allow_zone_drifting"] = (
original_firewalld_conf.get("allow_zone_drifting")
)
diff["permanent"]["added"]["allow_zone_drifting"] = current_firewalld_conf.get(
"allow_zone_drifting"
)
return diff
def check_for_diffs(
module,
warnings,
config_list,
replaced,
online_param=None,
__called_from_role_param=None,
):
"""
Process config_list with in-memory backend to check for differences.
This creates a firewall config from scratch and compares it with the
current config.
The diff is a dict suitable to display in diff mode.
custom_file_list is a list of xml files to be removed when
replaced is true.
Returns: (changed, diff, custom_file_list)
"""
# Build the desired config using InMemoryBackend with empty starting config
# We need to process all configs to build the complete desired state
online = online_param
backend = InMemoryBackend(module, online, start_empty=replaced)
# Build desired config with InMemoryBackend
for config in config_list:
# Create a temporary module params dict for this config
temp_params = copy.deepcopy(module.params)
base_spec = get_base_argument_spec()
for key, spec in base_spec.items():
temp_params[key] = config.get(key, spec.get("default"))
# Extract necessary parameters and set in backend for this config item
backend.permanent = temp_params.get("permanent")
backend.runtime = temp_params.get("runtime")
backend.zone = temp_params.get("zone")
backend.state = temp_params.get("state")
backend.timeout = temp_params.get("timeout", 0)
# Apply this configuration to the backend
# Pass the backend to process_single_config
process_single_config(
module,
warnings,
config_params=config,
backend=backend,
online_param=online,
__called_from_role_param=__called_from_role_param,
)
# Get the original and desired configs
original_config, permanent_config, runtime_config, default_zone, firewalld_conf = (
backend.get_configs()
)
custom_permanent_with_defaults = original_config.get(
"custom_permanent_with_defaults", {}
)
diff = get_diffs(
custom_permanent_with_defaults,
original_config.get("custom_runtime_with_defaults", {}),
original_config["default_zone"],
original_config["firewalld_conf"],
permanent_config,
runtime_config,
default_zone,
firewalld_conf,
True,
)
changed = any(
(
diff["permanent"]["added"],
diff["permanent"]["removed"],
diff["runtime"]["added"],
diff["runtime"]["removed"],
)
)
need_remove_custom_files = False
# I have seen cases where there are files in /etc/firewalld for custom permanent config, but
# the config is exactly the same as the default - this causes problems because it appears there
# is custom permanent config, but it isn't different - not sure how this happens - I have seen
# it in machine image builds, as if someone copied files from /usr/lib/firewalld
# into /etc/firewalld but didn't change them - the problem is that this causes the tools to
# report custom config where there isn't any actual customization or changes from the defaults
if replaced and (changed or custom_permanent_with_defaults):
default_diffs = {}
if custom_permanent_with_defaults:
# are the custom permanent settings the same as the defaults?
default_diffs = recursive_show_diffs(
custom_permanent_with_defaults, original_config.get("default", {}), None
)
need_remove_custom_files = changed or not default_diffs
return (
changed,
diff,
need_remove_custom_files,
original_config,
backend.set_interface_changed,
)
def process_replaced_config(
module,
changed,
need_remove_custom_files,
online,
):
# Remove existing firewalld configuration files
if not module.check_mode and (changed or need_remove_custom_files):
# Remove firewalld.conf
firewalld_conf_path = os.path.join(FIREWALLD_DIR, "firewalld.conf")
if changed and os.path.exists(firewalld_conf_path):
try:
os.remove(firewalld_conf_path)
module.debug("Removed %s" % firewalld_conf_path)
# ensure that firewalld.conf exists
fc = firewalld_conf(None)
try:
fc.read()
except Exception:
pass # the None causes an exception in read(), but this populates the fallback configuration
fc.filename = firewalld_conf_path
fc.write()
module.debug("Wrote fallback configuration to %s" % firewalld_conf_path)
except Exception as e:
module.fail_json(
msg="Failed to remove %s: %s" % (firewalld_conf_path, str(e))
)
# remove custom files even if nothing else has changed in order to clean up cases
# where there is a custom file which is exactly the same as a default file e.g.
# someone copied public.xml from /usr/lib/firewalld/zones to /etc/firewalld/zones
# and did not change it - it messes up the fact gathering because it is reported
# as a customization but it really isn't - in this case we report changed: false
# because the configuration didn't really change
if changed or need_remove_custom_files:
for root, dirs, files in os.walk(FIREWALLD_DIR):
for filename in files:
if filename.endswith(".xml"):
xml_file = os.path.join(root, filename)
if os.path.exists(xml_file):
try:
os.remove(xml_file)
module.debug("Removed %s" % xml_file)
except Exception as e:
module.warn(
"Failed to remove %s: %s" % (xml_file, str(e))
)
if online:
# Use FirewallClient to reload
try:
fw = FirewallClient()
fw.reload()
module.debug("Reloaded firewalld via FirewallClient")
except Exception as e:
module.fail_json(msg="Failed to reload firewalld: %s" % str(e))
else:
# Use firewall-offline-cmd (no reload needed in offline mode)
module.debug("Offline mode - no reload needed")
def main():
# Create the base argument spec
argument_spec = get_full_argument_spec()
# Add config_list parameter
argument_spec["config_list"] = dict(
required=False,
type="list",
elements="dict",
options=get_base_argument_spec(),
default=[],
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=(
("state", "present", ("zone", "target", "service"), True),
("state", "absent", ("zone", "target", "service"), True),
),
)
if not HAS_FIREWALLD:
module.fail_json(msg="No firewall backend could be imported.")
config_list = module.params["config_list"]
online = module.params["online"]
__called_from_role = module.params["__called_from_role"]
diff = {}
warnings = []
# Check for mutual exclusivity between config_list and other parameters
if config_list:
# Check if any non-config_list parameters are set (except online)
base_params = get_base_argument_spec()
current_params_set = []
for param_name, param_spec in base_params.items():
if param_name in ["online", "__called_from_role"]:
continue
param_value = module.params[param_name]
default_value = param_spec.get("default")
if param_value != default_value:
current_params_set.append(param_name)
if current_params_set:
module.fail_json(
msg="config_list cannot be used together with other module parameters. "
"Found these parameters set: %s" % ", ".join(current_params_set)
)
# Pre-scan config_list for previous="replaced"
has_replaced = any(
config.get("previous") == "replaced" for config in config_list
)
# Filter out all items with previous (keep items WITHOUT previous)
filtered_config_list = [
config for config in config_list if config.get("previous") != "replaced"
]
# Validate all configs
for i, config in enumerate(filtered_config_list):
if not isinstance(config, dict):
module.fail_json(
msg="config_list item %d must be a dictionary, got %s"
% (i, type(config).__name__)
)
# Validate config parameters against argument spec
for key in config:
if key not in base_params:
module.fail_json(
msg="config_list item %d contains invalid parameter '%s'. "
"Valid parameters: %s" % (i, key, ", ".join(base_params.keys()))
)
(
changed,
diff,
need_remove_custom_files,
original_config,
set_interface_changed,
) = check_for_diffs(
module,
warnings,
filtered_config_list,
has_replaced,
online_param=online,
__called_from_role_param=__called_from_role,
)
# NOTE: This means using previous: replaced with an interface change will not be
# completely idempotent because we have to remove the files and reload firewalld in order to see
# if the interface changes will actually change anything.
if has_replaced and (
changed or need_remove_custom_files or set_interface_changed
):
# This handles removing the files that need to be removed and reloading firewalld if necessary
process_replaced_config(
module,
changed,
need_remove_custom_files or set_interface_changed,
online,
)
if module.check_mode or (not changed and not set_interface_changed):
# Exit early because either check_mode is True and we don't want to apply changes,
# or no changes were made and no interfaces were changed, so we don't need to process
# any changes.
changed = changed if module.check_mode else False
module.exit_json(
changed=changed,
__firewall_changed=changed,
diff=diff,
short_circuit=True,
)
# From here on, something has changed, and we need to process those changes
# if set_interface_changed is True, we need to process the changes in order to see
# if the interfaces have changed
# Process each configuration in the list normally
for config in filtered_config_list:
# Process this configuration
if process_single_config(
module,
warnings,
config_params=config,
online_param=online,
__called_from_role_param=__called_from_role,
):
# Something changed
changed = True
# get the current config after processing the changes, then compare it to the original config
current_config = config_to_dict(module, detailed=True, online=online)
diff = get_diffs(
original_config["custom_permanent_with_defaults"],
original_config.get("custom_runtime_with_defaults", {}),
original_config["default_zone"],
original_config["firewalld_conf"],
current_config["custom_permanent_with_defaults"],
current_config.get("custom_runtime_with_defaults", {}),
current_config["default_zone"],
current_config["firewalld_conf"],
False,
)
if not changed:
changed = any(
(
diff["permanent"]["added"],
diff["permanent"]["removed"],
diff["runtime"]["added"],
diff["runtime"]["removed"],
)
)
module.exit_json(
changed=changed,
__firewall_changed=changed,
diff=diff,
short_circuit=False,
warnings=warnings,
)
else:
# Original single configuration mode
changed = process_single_config(
module,
warnings,
online_param=online,
__called_from_role_param=__called_from_role,
)
module.exit_json(
changed=changed,
__firewall_changed=changed,
short_circuit=False,
warnings=warnings,
)
#################################################
if __name__ == "__main__":
main()