module

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

check_mode_rule_changed?(dry_run_output : String, pre_rules : String, from_ip : String, to_ip : String) : Bool

Real ufw.py's check-mode rule decision over ufw --dry-run ... output, ported line-for-line (including its inverted-named filter_line_that_not_start_with, which actually KEEPS the lines that start with the pattern - the module's own bug, and the behavior the pre/post tuple diff silently depends on):

  • when every line of the dry-run output contains "Skipping", ufw made no change (e.g. "Skipping adding existing rule") - changed stays false;
  • otherwise the dry-run output's ### tuple ... lines are compared against the pre-probe grep - filtered to lines containing an ipv4/ipv6 literal when the rule's from/to ip STARTS with one of that family (Python re.match, i.e. anchored at the start), compared whole otherwise.
Source
default_command(value : String, direction : String | Nil) : String
Source
exec_failure_msg(stdout : String, stderr : String) : String

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 Ansible shows whenever the ufw CLI exits non-zero

  • e.g. a container without CAP_NET_ADMIN, where even ufw status verbose dies 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.
Source
logging_command(value : String) : String
Source
resolve_insert(insert : Int32, relative_to_cmd : String, numbered_status : String) : Int32 | Nil

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 Ansible drops the insert flag entirely in that case (the rule is just appended normally) instead of sending a command ufw would refuse.

Source
rule_command(params : Hash(String, String), dry_run : Bool = false, raw : Bool = false) : String

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

raw: true leaves ip/port/interface/insert values unquoted - the string real ufw.py records in commands: and then shlex.splits before running it, so a value with spaces or quotes (a whole list templated into port:) becomes several arguments there, not one.

Source
splitlines_keepends(s : String) : Array(String)

Python str.splitlines(keepends=True) over \n-separated text (ufw/grep output): a trailing newline terminates a line, a final chunk without one is still a line, and "" has no lines.

Source
state_command(state : String) : String | Nil
Source
version_parses?(output : String) : Bool

Real ufw.py's ufw_version(): parses the first non-empty line of ufw --version ("ufw 0.36.2") for major.minor[.rev]; anything else fails the module with Ansible's own fail_json wording ("Failed to get ufw version.").

Source