Krikri::PluginHelpers::UfwCommand
UfwCommand - pure logic for building the ufw command line for a
rule/state/default/logging change. No I/O here - the plugin itself
runs the resulting command and interprets its output.
Rule command shape verified against community.general's actual
ufw.py source (the "long format" documented directly in its own
comment): ufw [route] [delete | insert NUM] allow|deny|reject|limit [in|out on INTERFACE] [log] [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] [proto protocol] [app application] [comment COMMENT].
Class methods
Real ufw prints a line containing "Skipping" (e.g. "Skipping
adding existing rule") and exits 0 when a rule command is a
no-op - verified against community.general's actual ufw.py
source, which checks for exactly this substring
(filter_line_that_contains("Skipping", rules_dry)) to decide
changed in check mode. Used here as the changed signal for a
real (non-dry-run) application too, since replicating ufw's full
rule-tuple diffing logic would need actually-working netfilter
access to verify - not available in this project's Docker-based
compat harness (rootless podman container), so this is
source-verified but not further behavior-verified end-to-end the
way every other plugin in this codebase has been.
Real ufw.py's execute() fails with module.fail_json(msg=err or out, commands=cmds) - stderr wins, stdout is the fallback when
the failing command wrote nothing to stderr. Every ufw
invocation (pre/post status probes, the state/rule command
itself) goes through that helper, so this is the failure
message real Ansible shows whenever the ufw CLI exits non-zero
- e.g. a container without CAP_NET_ADMIN, where even
ufw status verbosedies with iptables' "Permission denied (you must be root)". This engine used to read only the probes' stdout and ignore their exit codes entirely, turning that exact situation into a false "Rules updated" success.
Resolves insert:/insert_relative_to: into the actual absolute
ufw insert NUM position, given ufw status numbered's own
output. zero (the default) is a pure passthrough - the caller
only needs this for the other four values. Algorithm (including
the "no ipv4/ipv6 rules yet" fallback positions and the
insert-past-the-end-means-no-insert-flag-at-all case) copied
field-for-field from community.general's actual ufw.py source,
not derived from the docs' prose - it's the one piece of this
plugin dense enough that reproducing it from the English
description alone would very likely have drifted from the real
rule-number arithmetic.
Returns nil if the resolved position would fall past the last
existing rule - real ufw rejects an insert number larger than the
maximum rule number, so real Ansible drops the insert flag
entirely in that case (the rule is just appended normally)
instead of sending a command ufw would refuse.
params keys read: route, delete, insert, rule, direction,
interface, interface_in, interface_out, log, from_ip, from_port,
to_ip, to_port, proto, name, comment. dry_run inserts ufw's own
--dry-run flag right after the binary name, matching its real
position in the "long format" (ufw [--dry-run] [route] ...).