module

Krikri::FactsGatherer

Perf item 2 - the fact-gathering body, lifted verbatim out of plugins/facts.cr so the fat plugin binary can link it and serve facts over the persistent daemon like every other module.

facts was the last plugin still excluded from both the fat binary and the daemon, and the reason was purely SHAPE: it had no *Plugin < BasePlugin class and no input = STDIN.gets_to_end trailer, which is what build.sh's fat-binary generator keys on. It gathers on every host in every play and is frequently the slowest single step of a warm run, so it paid a fresh ssh fork + remote process spawn for the one task nothing can skip.

Everything below is the ORIGINAL top-level code, unchanged except for being wrapped in this module (extend self keeps every internal call site - capture, gather_facts, the gather_*_facts family - resolving exactly as it did at top level). Wrapping matters: merged into one binary with 80+ other plugins, top-level def capture and a top-level re-opened lib LibC would be sharing a namespace with every one of them.

It is deliberately NOT reshaped into a BasePlugin subclass, which would have needed no generator change at all: BasePlugin#run_and_ capture returns a PluginResult, whose to_json round-trips every extra field through JSON.parse(value.to_json) - a serialize-then- reparse of the whole fact dict, on the exact hot path this item exists to make cheaper. It would also have added an always-empty msg key to a payload that has never carried one.

Constants

ALL_FAMILIES = MIN_FAMILIES + FAMILY_SUBSETS.keys
ANSIBLE_DOC_VERSION = (Krikri::ANSIBLE_TARGET_VERSION.split('.'))[0, 2].join('.')

Ansible's interpreter_discovery builds the help_text doclink from the RUNNING ansible-core's major.minor (get_versioned_doclink), so a 2.21.6 controller points at /ansible-core/2.21/. A hardcoded release here made the discovery warning diverge from real's byte for byte as soon as the emulated release moved on (issue #933's podman diff: real 2.21 vs krikri 2.19). Derived from the release krikri targets so it cannot drift again.

ANSIBLE_INTERFACE_TYPES = {"1" => "ether", "32" => "infiniband", "512" => "ppp", "772" => "loopback", "65534" => "tunnel"} of String => String

Ansible's LinuxNetwork.INTERFACE_TYPE map (network/linux.py): sysfs /sys/class/net/<iface>/type numbers to the fact string, unknown numbers reporting "unknown" - default_ipv4.type is sourced from the same file the same way (issue #133).

CHASSIS_TYPES = {1 => "Other", 2 => "Unknown", 3 => "Desktop", 4 => "Low Profile Desktop", 5 => "Pizza Box", 6 => "Mini Tower", 7 => "Tower", 8 => "Portable", 9 => "Laptop", 10 => "Notebook", 11 => "Hand Held", 12 => "Docking Station", 13 => "All In One", 14 => "Sub Notebook", 15 => "Space-saving", 16 => "Lunch Box", 17 => "Main Server Chassis", 18 => "Expansion Chassis", 19 => "Sub Chassis", 20 => "Bus Expansion Chassis", 21 => "Peripheral Chassis", 22 => "RAID Chassis", 23 => "Rack Mount Chassis", 24 => "Sealed-case PC", 25 => "Multi-system", 26 => "CompactPCI", 27 => "AdvancedTCA", 28 => "Blade", 29 => "Blade Enclosure", 30 => "Tablet", 31 => "Convertible", 32 => "Detachable", 33 => "IoT Gateway", 34 => "Embedded PC", 35 => "Mini PC", 36 => "Stick PC"}

Ansible's own SMBIOS chassis-type table behind ansible_form_factor.

DEFAULT_FACT_PATH = "/etc/ansible/facts.d"
DEFAULT_GATHER_TIMEOUT = 10
FACT_BINARY_EXTRA_DIRS = ["/sbin", "/usr/sbin", "/bin", "/usr/bin"]

Directories searched beyond $PATH for service-manager binaries - /sbin and /usr/sbin are routinely absent from a non-login shell's PATH, and that is where systemctl/initctl live on some distros.

FACTS_CONSUMED_PARAMS = ["ansible_python_interpreter"] of ::String

The same, for this engine's implicit Gathering Facts call: the executor hands the facts plugin the host's already-resolved ansible_python_interpreter, which real folds into the module's shebang rather than its argument spec.

FAMILY_SUBSETS = {"network" => ["network", "all_ipv4_addresses", "all_ipv6_addresses", "default_ipv4", "default_ipv6", "interfaces"] of ::String, "hardware" => ["hardware", "devices", "dmi", "processor", "processor_cores", "processor_count", "nvme"] of ::String, "mounts" => ["mounts"] of ::String, "ssh_pub_keys" => ["ssh_pub_keys", "ssh_host_pub_keys", "ssh_host_key_dsa_public", "ssh_host_key_rsa_public", "ssh_host_key_ecdsa_public", "ssh_host_key_ed25519_public"] of ::String, "virtual" => ["virtual", "virtualization_role", "virtualization_type", "virtualization_tech_guest", "virtualization_tech_host"] of ::String, "is_chroot" => ["is_chroot"] of ::String, "loadavg" => ["loadavg"] of ::String, "fibre_channel_wwn" => ["fibre_channel_wwn"] of ::String, "iscsi" => ["iscsi"] of ::String, "hostnqn" => ["hostnqn"] of ::String}

Which subset tokens map to which krikri gatherer family - Ansible's aliases_map (each collector's _fact_ids): asking for a single fact id like all_ipv4_addresses turns on that collector's whole family, exactly as Ansible's fact_id -> collector map does. The min-bundle subset names (distribution, python, user, ...) are absent: they resolve to the min bundle itself, which Ansible always gathers first anyway.

GATHER_FACTS_CONSUMED_PARAMS = ["parallel"] of ::String

Task args the gather_facts ACTION plugin consumes itself and never forwards to the fact module it runs (gather_facts.py pops parallel off the task args before building the module args). A direct setup: task's own parallel: is NOT consumed - it reaches setup's argument spec and fails there like any other unknown key.

INTERPRETER_PYTHON_FALLBACK = ["python3.13", "python3.12", "python3.11", "python3.10", "python3.9", "python3.8", "/usr/bin/python3", "python3"]

Ansible's INTERPRETER_PYTHON_FALLBACK default (ansible-core 2.19.11, ansible/config/base.yml) - the ordered candidate list its interpreter discovery probes. Discovery is deliberately dumb: one command -v per candidate through the connection's /bin/sh, and the first absolute path in the output wins, /usr/bin/python3 when nothing does. That is why a Ubuntu 22.04 target discovers the VERSIONED /usr/bin/python3.10 and not whatever its python3 resolves to - the value this engine's python facts used to stamp into discovered_interpreter_python by probing under find_executable("python3"), which real never reports (issue #374: the divergence is observable from the controller, where a connection: local task executes the discovered path).

MIN_FAMILIES = ["min", "local"] of ::String

The families "min" covers in this engine. Ansible's minimal_gather_subset also includes 'local' (the ansible_local fact_path collector), which this tracks as its own family entry so !local can drop just the custom-facts scan without touching the rest of min. "min" itself is the bookkeeping name for the six always-on gatherers in gather_facts.

SETUP_OPTIONS = ["fact_path", "filter", "gather_subset", "gather_timeout"] of ::String

setup's own argument-spec options (ansible.builtin.setup's four documented params), in the order Ansible's UnsupportedError message sorts them into "Supported parameters include:".

SSH_PUB_KEY_DIRS = ["/etc/ssh", "/etc/openssh", "/etc"] of ::String

SshPubKeyFactCollector's keydir search order.

VALID_SUBSETS = ["all_ipv4_addresses", "all_ipv6_addresses", "apparmor", "architecture", "caps", "chroot", "cmdline", "date_time", "default_ipv4", "default_ipv6", "devices", "distribution", "distribution_major_version", "distribution_release", "distribution_version", "dns", "effective_group_ids", "effective_user_id", "env", "facter", "fibre_channel_wwn", "fips", "hardware", "interfaces", "is_chroot", "iscsi", "kernel", "kernel_version", "loadavg", "local", "lsb", "machine", "machine_id", "mounts", "network", "nvme", "ohai", "os_family", "pkg_mgr", "platform", "processor", "processor_cores", "processor_count", "python", "python_version", "real_user_id", "selinux", "service_mgr", "ssh_host_key_dsa_public", "ssh_host_key_ecdsa_public", "ssh_host_key_ed25519_public", "ssh_host_key_rsa_public", "ssh_host_pub_keys", "ssh_pub_keys", "system", "system_capabilities", "system_capabilities_enforced", "systemd", "user", "user_dir", "user_gecos", "user_gid", "user_id", "user_shell", "user_uid", "virtual", "virtualization_role", "virtualization_tech_guest", "virtualization_tech_host", "virtualization_type"] of ::String

Every subset name ansible-core 2.19.4 accepts - the exact list its own Bad-subset failure message enumerates (captured live from setup on this machine). krikri implements only the network/ hardware/mounts families plus the min bundle; names that Ansible resolves to collectors krikri has no implementation for (virtual, dns, selinux, ...) are ACCEPTED but gather nothing extra, because failing them would break every role that uses a valid subset name this engine simply has no facts for.

Instance methods

apply_fact_filter(facts : FactSet, patterns : Array(String)) : FactSet

filter - Ansible's fnmatch (shell-style glob) filter over the TOP-LEVEL fact keys only, applied after gathering. Empty patterns mean no filter (live-verified: filter="" returns everything).

Source
capture(command : String, args : Array(String) = [] of String) : String

Runs command with args directly (no shell), capturing stdout only (stderr discarded) - equivalent to `command 2>/dev/null` but without forking an intermediate /bin/sh -c. Returns "" if the binary can't be found or execution otherwise fails, matching the empty-output behavior a missing command produces under the shell.

Source
capture_merged(command : String, args : Array(String) = [] of String) : String

Same as capture, but merges stderr into stdout - equivalent to `command 2>&1`. Used for python --version, which some Python builds print to stderr instead of stdout.

Source
dash_shell?

dash words a missing command as "/bin/sh: 1: X: not found" while bash words it as "/bin/sh: line 1: X: No such file or directory" - ansible shows whichever of the two the target's /bin/sh really is, so the flavour has to be detected on the machine running this code.

Source
detect_pkg_mgr

Detect whether we're running inside a container/VM, following the same heuristics Ansible's fact gathering uses. Returns the virtualization type name (e.g. "docker", "lxc", "kvm", "xen"), or "None" (the exact string Ansible uses) when running on bare metal / a plain host. ansible_pkg_mgr / ansible_facts.pkg_mgr - which package manager Ansible's own pkg_mgr.py fact module reports, entirely unset before this (found via openstack.ansible-hardening's own include_tasks: "{{ ansible_facts['pkg_mgr'] }}.yml" - the role's main OS-dispatch point, resolving to the literal "undefined.yml" and failing the include outright, taking the rest of that STIG control file's tasks down with it). Ansible's own detection checks a longer, more exhaustive list of package-manager binary paths and has extra dnf-vs-yum-symlink disambiguation; this covers the package managers real roles actually gate on (apt/dnf/yum/zypper/pacman/apk/pkgng), checked in the same dnf-before-yum priority Ansible uses so a modern RHEL system (where /usr/bin/yum is often just a symlink to dnf) reports "dnf".

Source
detect_virtualization
Source
discover_python_interpreter

Runs ansible's discovery bootstrap on THIS machine (the plugin executes on the target for a remote gather, so "this machine" is the target there, exactly like real's _low_level_execute_command through the connection). Returns {interpreter, found_any}: found_any distinguishes ansible's two fallback shapes - "no interpreters found" (warning, early return, no second warning) from "unexpected bootstrap output" (which real treats as the fallback with the ordinary discovered-interpreter warning).

Source
fnmatch_to_regex(pattern : String) : Regex

Python fnmatch.translate's glob dialect: *, ?, [seq], [!seq] - an unterminated [ is a literal. Case-sensitive (posix fnmatch).

Source
fqdn_from_getent_hosts(output : String) : String

socket.getfqdn()'s final-step parse lives in PluginHelpers::Getfqdn now, shared with the hostname plugin, which must produce the same ansible_fqdn at its own returned-facts layer (issue #585: the two disagreed and the role's warm run diverged).

Source
gather_cmdline_facts(facts)

/proc/cmdline facts - Ansible's CmdLineFactCollector part of real min output (podman-diff setup case, real W2): ansible_cmdline collapses duplicate keys (later token wins), ansible_proc_cmdline turns them into lists, and a flag without "=" is True. Empty/missing /proc/cmdline sets NEITHER key (Ansible's collector returns {} and skips both facts).

Source
gather_date_time_facts(facts)
Source
gather_device_facts(facts)

Block-device facts - the ansible_devices dict (keyed by device name) ansible-core's Linux hardware collector ALWAYS populates by scanning /sys/block/*. Found via Tecnativa.hetzner_rescue_installimage's templates/autosetup.j2 ({% for device in ansible_devices if device.startswith("sd") ... %}): with the fact never set, the loop died with "can't iterate over undefined" and killed the role's "configure installation" task, which Ansible completes (iterating a missing fact is never the case there - its setup module defines the key even as an empty dict). Deliberately NOT conditioned on non-empty, unlike ansible_mounts above: templates need the key to exist and be iterable even when the scan finds nothing (minimal containers) - making it omit-when-empty would reintroduce exactly this bug.

Source
gather_dns_facts(facts)

ansible_dns - Ansible's DnsFactCollector over /etc/resolv.conf: nameserver lines append to "nameservers", domain/search/sortlist as scalar/list, options as key:value or bare-True flags. The KEY is always set - an empty/comment-only resolv.conf yields {}, never an absent ansible_dns (real min output carries it on every host, podman-diff setup case).

Source
gather_environment_facts(facts)
Source
gather_facts(subset : Array(String) = [] of String, remote_connection : Bool = false, gather_timeout : Int64 | Nil = nil, fact_path : String | Nil = DEFAULT_FACT_PATH, python_interpreter : String | Nil = nil) : FactSet

Gather all system facts gather_subset: - which families of facts to collect. Tokens are Ansible's: all, min, hardware, network, mounts, the per-fact aliases under FAMILY_SUBSETS, plus a leading "!" to subtract. Later tokens win, unknown positive tokens fail (BadSubsetError), and "min" is the floor exactly as in Ansible - which means !all still yields the min set, while !all,!min (or a bare !min) yields nothing but the gather_subset/module_setup meta keys, live-verified against 2.19.4.

Subsetting exists to skip the EXPENSIVE families: !hardware avoids reading every block device, !mounts avoids statting every mount.

Source
gather_fibre_channel_wwn_facts(facts)

ansible_fibre_channel_wwn - Ansible scans /sys/class/fc_host/*/node_name + port_name ("0x..." strings); no FC adapters (containers, most VMs) yields []. The KEY is still set under the subset that selects it (real W5 carries it with [] on hosts without FC hardware) - podman-diff setup case.

Source
gather_hardware_facts(facts)
Source
gather_hostname(facts)

Gather hostname facts

Source
gather_hostnqn_fact(facts)

ansible_hostnqn - Ansible reads /etc/nvme/hostnqn ("" when missing). Missing entirely before - podman-diff setup case (real W5).

Source
gather_is_chroot_fact(facts)

ansible_is_chroot - Ansible's IsChrootFactCollector compares /proc/1/root's resolved inode/device against /: same (the usual case, PID 1 lives in this root) is False, different is True. Missing entirely before - podman-diff setup case (real W5, all).

Source
gather_iscsi_fact(facts)

ansible_iscsi_iqn - Ansible reads the InitiatorName= line out of /etc/iscsi/initiatorname.iscsi, defaulting to "" when the file (or the entry) is absent. Missing entirely before - podman-diff setup case (real W5).

Source
gather_loadavg_facts(facts)

ansible_loadavg - Ansible's LoadAvgFactCollector over /proc/loadavg's first three fields. Missing entirely before - podman-diff setup case (real W5, all).

Source
gather_local_facts(fact_path : String | Nil) : Hash(String, JSON::Any)

fact_path - Ansible's local facts mechanism Observed behavior: every *.fact file in fact_path (real default /etc/ansible/facts.d) becomes a key under ansible_local. Executable files are RUN and their stdout parsed; the rest are read in place. Content must parse as JSON, else as ini (section-REQUIRED - a bare key=value with no [section] header is configparser's MissingSectionHeaderError, live-verified, and yields the same "error loading facts as JSON or ini - please check content:" error string Ansible stores as the fact's value); unparseable content is stored as that error string, never fatal.

Source
gather_mount_facts(facts)

Mount facts - a list of dicts, one per mounted filesystem, matching Ansible's ansible_mounts shape (mount/device/fstype/opts are the fields roles like os_hardening read). Parsed from /proc/self/mountinfo rather than forking mount, and bounded to real bind/devtmpfs noise that roles filter on themselves.

Source
gather_mount_space_stats(mountpoint : String) : Hash(String, Int64 | String)

Ansible's own ansible_facts['mounts'] entries always include space/inode statistics (size_total/size_available/block_size/ block_total/block_available/block_used/inode_total/ inode_available/inode_used, from os.statvfs() on each mountpoint) alongside mount/device/fstype/opts - this plugin only ever populated the latter, so any role reading the former (robertdebock.diskspace's whole purpose: item.size_available | int >= kilobytes_available | int) saw an undefined field and either crashed or - worse - silently never actually checked anything, since the role's own when: mount.name == item.mount guard still matched the real mountpoint correctly; the comparison inside the assert is what broke. Reads statvfs(2) directly through LibC (declared at the top of this file next to uname) instead of forking a stat -f subprocess per mount - identical numbers, just fetched via a syscall instead of a subprocess. Same fields Ansible's own os.statvfs() reads: block_size (f_frsize, what stat -f %S reports) block_total (f_blocks) block_free, all users (f_bfree) block_available, non-root (f_bavail) inode_total (f_files) inode_free, non-root (f_favail)

Source
gather_network_facts(facts)

Section-annotation wrapper: a crash inside one gatherer's parsing (e.g. the round-5210000 ktechmidas.openvpn warm run's bare "Index out of bounds" from somewhere in the network section after the role's own tun interface/service state changed the host) surfaces as "Facts gathering failed: <msg>" with no hint which section died. Annotate the message with the section name so the next occurrence pinpoints itself.

Source
gather_os_facts(facts)

Gather OS facts

Source
gather_python_facts(facts, remote_connection : Bool = false, interpreter_override : String | Nil = nil)
Source
gather_ssh_pub_keys_facts(facts, keydirs : Array(String) = SSH_PUB_KEY_DIRS)

ansible_ssh_host_key_{algo}_public(+keytype) - Ansible's SshPubKeyFactCollector (ssh_pub_keys). Never implemented before - znerol.ssh_kba round 5466303: the role's server-host-key chain reads hostvars[..]["ansible_ssh_host_key*"] behind | default(False) guards, which all went false, collapsing ssh_kba_server_host_names_and_keys to [] and skipping its | length > 0 include gate where ansible-playbook runs all six loop items. (Round 5460170's depth-cap fix was real but masked this: the loop ran empty for a different reason on every real host.)

Mirrors the collector's exact walk: dirs in order (/etc/ssh, /etc/openssh, /etc), algos (dsa, rsa, ecdsa, ed25519), and - subtle - the early return fires PER FACT NAME, so an algo the first dir lacked keeps falling through to the later dirs while the algos it DID find stop the search immediately. A file that exists but yields fewer than two whitespace-separated tokens is skipped (real would raise mid-collection; no real .pub file does this).

Source
gather_system_capabilities_facts(facts)

ansible_system_capabilities / _enforced - Ansible's SystemCapabilitiesFactCollector via capsh --print: its "Current:" line decides both - the bare "=ep" bounding set means unenforced, anything else means enforced with that capability list; no capsh binary (or a failing run) leaves both keys at Ansible's literal "N/A" defaults. Missing entirely before - podman-diff setup case (real W2 min carries both keys).

Source
gather_user_facts(facts)
Source
gather_virtualization_facts(facts)

virtualization facts - moved out of the min gatherers into their own family: Ansible's VirtualFactCollector runs under the 'virtual' subset, which !all (min only) never selects (real W2 min output has NO virtualization facts, W5 all does - podman-diff setup case), so reporting them under min made this engine's !all result diverge by two keys. The tech_guest/tech_host lists Ansible also reports (as sets, serialized as lists) were missing entirely.

virtualization_role ("guest"/"host"/"NA") - entirely missing before this, found benchmarking Ansible-Security-Compliance's rhel7-role-hipaa (round823): its own audit-rule tasks gate on ansible_virtualization_role != "guest" or ansible_virtualization_ type != "docker" (skip certain host-only audit rules on a container/VM guest) - Ansible resolves this fine on a real cloud VM (role: "guest"), this engine raised "Error while evaluating conditional: 'ansible_virtualization_role' is undefined" and crashed the whole run outright instead of just this one task's when:. This engine's own #detect_virtualization never distinguishes hypervisor-host detection from guest detection (Ansible's own host-side checks - a populated /etc/xen/, a running libvirtd, etc - are rare in practice and not implemented here), so "host" is never reported; every detected type maps to "guest", matching the overwhelming common case (a real role's target is virtualized, not the hypervisor itself).

Source
interpreter_fallback_repr

Python repr() of the fallback list, for ansible's "No python interpreters found for host 'x' (tried [...])." warning text.

Source
interpreter_missing?(path : String) : Bool

Whether path cannot be executed on this machine - the check Ansible's interpreter discovery effectively performs before failing with rc 127. A leading "~/" is the invoking user's home; anything containing "/" must exist as an executable file, while a bare name must resolve on PATH. "auto" and its variants are discovery requests, not paths - Ansible never reports them missing.

Source
interpreter_missing_report(host_name : String, interpreter : String, connection_addr : String, remote : Bool, dash_shell : Bool = false) : String

The exact block ansible-playbook prints when the implicit setup module dies with rc 127 on a missing interpreter (live-verified output): the failed-task banner, the cause chain, the sub-event box carrying the module's own message, and the fatal line with ansible's failed_modules payload. The fatal line is returned uncoloured as its last line - the executor applies the red tint there, like every other fatal line, without touching the rest.

module_stdout/module_stderr differ by transport AND by target shell: over a real SSH round trip the interpreter error is the remote shell's (dash says "/bin/sh: 1: ... not found", bash says "/bin/sh: line 1: ... No such file or directory") and the connection-closed notice lands in stderr, while on ansible_connection=local the controller's own /bin/sh raises "not found" on stderr and stdout is empty.

The JSON's key order follows ansible's observed failed_modules payload exactly, so it is written with JSON.build in that order rather than left to a hash's iteration - the escaped \r\n inside the shell strings is how ansible displays them inside the JSON.

Source
os_release_content

The RAW text of whichever os-release file #parse_os_release used - Ansible's distribution-file parsers match substrings against the whole file, not against parsed key/value pairs ("Mint" in data), so a faithful port needs the original text.

Source
parse_cmdline(data : String, multi : Bool) : Hash(String, JSON::Any)
Source
parse_container_env(environ : String) : String | Nil

Parses PID 1's /proc/1/environ content (NUL-separated key=value entries) for a container= marker, matching Ansible's own container=lxc/container=podman/generic-container=. priority order. Only lxc and podman get their own specific virtualization_type - EVERY other non-empty value (docker, oci, systemd-nspawn, ...) normalizes to the literal string "container", never the raw env value itself (if re.search('^container=.', line): virtual_facts ['virtualization_type'] = 'container' - it does not capture or reuse the matched value). Previously this returned the raw value verbatim, so a Kata VM whose guest happened to carry container= docker in PID 1's environ (a leftover from the base rootfs image having been built via podman build/Containerfile, even though Kata boots a real guest kernel with no actual container runtime inside it) reported "docker" - matching a role's virtualization_ type == "docker" when: check that Ansible (which reports the generic "container") correctly left false. Found benchmarking juju4.auditd's own "Not in container" block guard. Pulled out of #detect_virtualization as a pure function so it's testable without real /proc access. Returns nil when no container= entry is present at all (the plain-host case).

Source
parse_discovery_output(raw : String) : Tuple(String, Bool)

The FOUND/ENDFOUND parsing half of discover_interpreter: only absolute paths count (ansible's if interp.startswith('/')), the first one wins, and output that does not match the bootstrap wrapper at all falls back to /usr/bin/python3 the way real's caught-ValueError path does.

Source
parse_dns_content(content : String) : Hash(String, JSON::Any)
Source
parse_ipv4_addr_output(output : String) : Array(Hash(String, JSON::Any))

Parses raw ip -o -4 addr show text into the ansible_<iface> per- address entry hashes (address/netmask/network, broadcast on "brd" lines). Lives here as a pure text-in/text-out method so the line shapes that killed the gatherer on the round-5300009 ktechmidas.openvpn warm run are unit-pinnable without a live ip binary: back before the annotation pinpointed "while gathering network", its only observable symptom was the bare "Index out of bounds" above - parse time was the first place a changed-upon-warm-network-state host could feed unexpected text to crystal's strict indexing. Every line is parsed OR skipped, never crashed: a line whose address is not exactly 4 numeric octets (a no-address or partially-configured tun/bridge/wg left behind by the role's own changes), or that carries no /prefix at all, is dropped the way Ansible's own per-line continue drops it.

Source
parse_ipv6_addr_output(output : String) : Array(JSON::Any)
Source
parse_lsb_release(content : String) : Hash(String, String)

Parse an /etc/lsb-release payload into the ansible_lsb dict as a pure function so it's testable without the real file (extracted from #gather_os_facts, same shape as #parse_container_env).

Source
parse_os_release
Source
parse_processor_list(cpuinfo : String) : Array(String)

ansible_processor - ansible-core's Linux get_cpu_facts appends the value of EVERY /proc/cpuinfo line whose key is exactly one of 'processor', 'vendor_id', 'model name', 'Processor', 'cpu' or 'Vendor' to ONE flat list, in file order: per x86 logical CPU that is [index, vendor, model] - roles-ansible.amdgpu_firmware's when: ansible_processor[1] == 'AuthenticAMD' reads the vendor slot, and the old "first 'model name' string alone" shape made every such list index die with "undefined". ARM and other layouts contribute whatever of those keys exist. Lines without a colon still contribute "" when their whole text is a key, matching ansible's split()/IndexError handling.

Source
read_file_stripped(path : String) : String

The /proc and /sys file reads that used to go through capture("cat", ...) now read directly - no fork for what is a plain read(2). The contract is capture("cat", ...)'s exactly: stripped content, or "" when the file is missing or unreadable.

Source
real_mount_device_kept?(device : String, fstype : String) : Bool

Ansible's own keep-or-skip rule for a mount's device field (hardware/linux.py get_mount_facts, live-verified): local device paths ("/dev/...", "/...") and NFS-style exports ("host:/path") are kept; pseudo-filesystem devices (overlay, proc, tmpfs, udev, cgroup, ...) are dropped, as is any fstype of exactly "none". Deliberately NOT private: the podman-diff setup_edge_cases_v2 FS3 divergence (Ansible reports an empty ansible_mounts inside a container, krikri reported every /proc/mountinfo entry) is pinned per-rule in facts_mount_network_scoping_spec.cr against this.

Source
resolve_enabled_families(tokens : Array(String)) : Set(String)

Ansible's get_collector_names, narrowed to the families this engine implements: 'min' is prepended unconditionally, "min"/"all" (and their negations) are special, a positive unknown token FAILS (BadSubsetError, mirroring Ansible's TypeError), a negated unknown token is ignored, and an empty resolution widens to everything. Later tokens win. Returns the set of families to gather.

Source
run(config : JSON::Any | Nil) : String

The former # Entry point block, with the two differences that make it serve both callers: config arrives already parsed (the daemon hands over a JSON::Any; the standalone driver parses STDIN itself), and the JSON is RETURNED rather than printed, since the daemon frames the response itself. nil means "no config at all", which the standalone path can legitimately see and which means Ansible's argument-spec defaults (gather_subset=all, gather_timeout=10, no filter, fact_path=/etc/ansible/facts.d).

Source

Nested types