3731 lines
142 KiB
Python
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()
|