Krikri::PluginHelpers::FirewalldCommand
FirewalldCommand - pure logic for building firewall-offline-cmd
command lines. No I/O here - the plugin itself runs the resulting
commands.
--zone=<zone> --query-<thing>=<value> exits 0/prints "yes" if
present, exits 1/prints "no" if absent - verified empirically
against a real firewall-offline-cmd (firewalld 2.1.1) in a real
container, since this behavior isn't documented in ansible-doc at
all (it belongs to firewall-offline-cmd itself, a companion CLI
tool, not the Ansible module).
service removal is a real, confirmed quirk - but only on
firewall-offline-cmd: its plain --remove-service is a legacy
"lokkit" option that can't be combined with --zone= at all (real,
verified error: "Can't use lokkit options with other options"), so
the zone-scoped removal form there is --remove-service-from-zone=.
The live firewall-cmd has NO --remove-service-from-zone option
at all (real, verified error: "firewall-cmd: error: unrecognized
arguments: --remove-service-from-zone=http", round994003 kop_rocky)
and takes plain --remove-service= with --zone= like every other
thing. port/rich-rule/source/masquerade don't have this
quirk on either binary; their plain --remove-<thing>= forms work
fine with --zone=.
The ZoneXml section below is the direct zone-config-file backend for offline mode: Ansible.posix.firewalld's offline mode does NOT shell out to firewall-offline-cmd at all - it uses firewalld's own Python Firewall(offline=True), which loads the /usr/lib/ firewalld + /etc/firewalld zone XML into memory and writes changes back to /etc/firewalld/zones/<zone>.xml. firewall-offline-cmd, by contrast, dies entirely in environments where its protocol validation can't resolve entries like 'esp' (getprotobyname('esp') fails in a slim container), so a CLI-based offline backend diverges from Ansible in exactly the containerized hosts this project targets. These helpers operate on the XML file CONTENT only - the plugin owns the reads/writes/paths.
Constants
Things whose add/remove/query flags take NO value at all
(--add-masquerade, not --add-masquerade=true) - verified
empirically against a real firewall-offline-cmd (firewalld
2.3.1) in a real container for icmp_block_inversion/forward too,
the same way masquerade originally was.
Class methods
A <forward-port> element's identifying attributes for a port_forward entry dict (port/proto required, toport required, toaddr optional and simply absent from the element when not given - matching the compound-value shape ForwardPortTransaction builds).
Builds the compound port=X:proto=Y:toport=Z[:toaddr=W] value
Ansible's own ForwardPortTransaction builds from a
port_forward: entry (a dict with port/proto/toport
required, toaddr optional and simply omitted from the value
when absent - verified against observed behavior and
live against a real firewall-offline-cmd, firewalld 1.3.3).
Returns {value: nil, error: "..."} with the exact error message
Ansible raises (checked in the same port/proto/toport order
the Ansible module checks them) when a required key is missing, or
{value: "port=...", error: nil} on success.
binary selects the runtime/live-daemon CLI (firewall-cmd,
talking to a running firewalld over D-Bus) vs the on-disk XML
editor (firewall-offline-cmd, no daemon needed) - the flags
after the binary are identical between the two.
Returns nil if none or more than one "thing" param is present - matches Ansible's own mutually_exclusive constraint (exactly one of service/port/rich_rule/source/masquerade/etc per task).
Serialized zone XML with the element added, or nil if it's already present (query-then-add stays the caller's idempotency primitive, mirroring the CLI path's query exit code).
The XML element name + identifying attributes each "thing" serializes to inside a zone config file. The compound value shapes are firewalld's own file format: port is "N/proto" split across the port/protocol attributes, everything else maps attribute-for-attribute. rich_rule is absent here on purpose: its string form parses through firewalld's own Rich_Rule into arbitrarily nested <rule> XML, and a hand-rolled subset would break query canonicalization (attribute order/equivalence), so it stays on the firewall-offline-cmd path.
Does the zone XML already contain the element? (the query step - exactly one element matching name + every identifying attribute)
Serialized zone XML with the element removed, or nil if it wasn't present.
Ansible's ServiceTransaction asks the daemon for the ZONE'S WHOLE
service list and tests membership (service in self.fw.getServices(self.zone)) - it never asks "is this one
service enabled". That is observable: --query-service=<name>
rejects a name that is not a defined service outright ("Error:
INVALID_SERVICE: <name>", with no zone context), so using it as
the idempotency probe made krikri fail EARLY - before Ansible's
transaction appends its context msg and before the permanent leg
reports the daemon's own zone-context error (round996006
firewalld_fail). --zone=<zone> --list-services is what
firewall-cmd itself documents as "List services added" for that
zone (firewalld's own daemon comment on getServices: "because
is called by firewall-cmd --zone --list-services"). It prints the
names SPACE-SEPARATED on ONE line (dhcpv6-client http ssh), live
on Rocky 9 - the first version of #service_list assumed one per
line and so never saw an enabled service, reporting changed
on every idempotent re-enable (round 997006).