module

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: --remove-service (no -from-zone suffix) 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") - the zone-scoped removal form is --remove-service-from-zone=. port/rich-rule/source/ masquerade don't have this quirk; their plain --remove-<thing>= forms work fine with --zone=.

The ZoneXml section below is the direct zone-config-file backend for offline mode: real 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 real 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

NO_VALUE_THINGS = ["masquerade", "icmp_block_inversion", "forward"] of ::String

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.

SUPPORTED_THINGS = ["service", "port", "rich_rule", "source", "masquerade", "interface", "icmp_block", "protocol", "icmp_block_inversion", "forward"] of ::String

Class methods

add_command(zone : String, thing : String, value : String, binary : String = "firewall-offline-cmd") : String
Source
flag_name(thing : String) : String
Source
forward_port_add_command(zone : String, value : String, binary : String = "firewall-offline-cmd") : String
Source
forward_port_element(entry : JSON::Any) : Tuple(String, Hash(String, String))

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).

Source
forward_port_query_command(zone : String, value : String, binary : String = "firewall-offline-cmd") : String
Source
forward_port_remove_command(zone : String, value : String, binary : String = "firewall-offline-cmd") : String
Source
port_forward_value(entry : JSON::Any) : NamedTuple(value: String | Nil, error: String | Nil)

Builds the compound port=X:proto=Y:toport=Z[:toaddr=W] value real 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 the real module's own source and live against a real firewall-offline-cmd, firewalld 1.3.3). Returns {value: nil, error: "..."} with the exact error message real Ansible raises (checked in the same port/proto/toport order the real module checks them) when a required key is missing, or {value: "port=...", error: nil} on success.

Source
query_command(zone : String, thing : String, value : String, binary : String = "firewall-offline-cmd") : String

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.

Source
remove_command(zone : String, thing : String, value : String, binary : String = "firewall-offline-cmd") : String
Source
thing(params : Hash(String, String)) : Tuple(String, String) | Nil

Returns nil if none or more than one "thing" param is present - matches real Ansible's own mutually_exclusive constraint (exactly one of service/port/rich_rule/source/masquerade/etc per task).

Source
zone_add(content : String, element : String, attrs : Hash(String, String)) : String | Nil

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).

Source
zone_element(thing : String, value : String) : Tuple(String, Hash(String, String))

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.

Source
zone_query(content : String, element : String, attrs : Hash(String, String)) : Bool

Does the zone XML already contain the element? (the query step - exactly one element matching name + every identifying attribute)

Source
zone_remove(content : String, element : String, attrs : Hash(String, String)) : String | Nil

Serialized zone XML with the element removed, or nil if it wasn't present.

Source
zone_set_target(content : String, target : String) : String

Serialized zone XML with the zone root's target attribute set (or removed for "default" - a zone's target isn't optional the way an entry is, absence IS "default").

Source