host

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Implements the "host" Verb (design-docs/HostVerb.md): a standalone, five-tab program showing everything Tangsible can determine about one host - live gathered facts, inventory group membership, which plays would run for it, its own host_vars files, and the raw "ansible-inventory --host" dump - entirely separate from the run/rerun/role verbs' own live tree UI, the same way "template" is (template.go).

Index

Constants

View Source
const (
	RecentRunsScanLimit = 20
	RecentRunsShowLimit = 5
)

RecentRunsScanLimit/RecentRunsShowLimit bound fetchRecentRuns' (design-docs/HostVerb.md "New ideas") own state.toml scan: finding "the last RecentRunsShowLimit times this host was touched" means replaying candidate invocations, newest first, until that many matches turn up - unbounded, that could mean opening every invocation of every playbook ever run in the project for a rarely-targeted host. RecentRunsScanLimit caps how many of the most recent invocations project-wide are ever even looked at; RecentRunsShowLimit stops the scan early the moment enough matches are already found.

View Source
const FactCacheCanaryKey = "ansible_architecture"

FactCacheCanaryKey is checked against runAnsibleInventoryHost's own output to decide whether a usable, fresh fact-cache entry already exists for this host - "ansible_architecture" is part of ansible's own default (min) gather_subset and essentially always present whenever any real gather has ever happened at all, regardless of which further, more specific subsets (hardware/network/virtual/...) were also gathered - a reasonable single presence check rather than requiring every individual field formatHostSummary might want.

View Source
const HostPingStubYAML = "- hosts: all\n  gather_facts: false\n  ignore_unreachable: true\n  tasks:\n    - name: ping\n      ansible.builtin.ping:\n"

HostPingStubYAML backs the host list's own connectivity dot (design-docs/HostVerb.md's "New ideas": green if ansible.builtin.ping succeeds, red otherwise) - a real `ansible.builtin.ping` task, run through the same "ansible-playbook + bundled callback plugin" path as every other live fetch in this file, deliberately not a raw ICMP/DNS/ SSH-port check from wherever tangsible itself happens to run: those bypass Ansible's own connection plugin entirely (jump hosts, non-standard ports, connection: local/docker/winrm) and can't tell you anything about whether Ansible itself could actually manage the host. ignore_unreachable matches hostSummaryStubYAML's own stub for the identical reason (moot given --limit always narrows to one host, but harmless and consistent).

View Source
const HostSummaryStubYAML = "" /* 131-byte string literal not displayed */

HostSummaryStubYAML is the play design-docs/HostVerb.md's Summary tab uses to gather live facts for one host: an *explicit* `ansible.builtin. setup:` task, not the play-level `gather_facts: true` shorthand this originally used. That original version had a real, reported bug: with `gathering = smart` configured in ansible.cfg (a common setup) and a fact cache already warm for the host - populated by any prior playbook run at all, not necessarily this one - the *implicit* "Gathering Facts" task `gather_facts: true` inserts is silently skipped altogether, producing zero jsonl output for it: no task-start, no runner event, nothing - confirmed empirically by reproducing the exact reported symptom (a real ansible.cfg with `gathering = smart` + `fact_caching = jsonfile`, cache warmed by a separate, unrelated playbook run first). `gather_facts: true`'s own doc-comment claim that this transparently respects the fact cache "with zero special-casing" was simply wrong: smart gathering doesn't mean "consult the cache, but still report"; it means "skip entirely, sometimes with no observable event at all." An *explicit* task calling the `setup` module directly doesn't have this problem - it's an ordinary task like any other, always executes, always fires a real event (confirmed the same way, against the same warmed cache) - `gathering`'s smart-skip logic only ever applies to the auto-inserted implicit task the `gather_facts:` play keyword creates, never to a task the playbook actually writes out itself. This still benefits from a configured fact cache exactly as intended, just at the module's own internal level (a fresh `setup` run still writes/reads the cache) rather than by skipping the task pre-emptively. ignore_unreachable matches the "template" Verb's own stub (template.go/writeTemplateStub) - moot in practice since --limit always narrows this to exactly one host, but harmless and consistent.

Variables

View Source
var HostDetailTabNames = []string{"Summary", "Groups", "Plays", "host_vars", "Everything known"}

HostDetailTabNames is the fixed tab order buildHostDetailPrimitive always builds a fresh TabbedPane in - shared with runHostsListTUI's own n/N host-navigation (tabIndexByName), which needs to know this order to restore the same tab after rebuilding a brand new TabbedPane for the newly-selected host, since TabbedPane itself has no "jump to tab by name" method, only relative Next()/Prev().

View Source
var HostKeyTypeOrder = []string{"ed25519", "ecdsa", "rsa", "dsa"}

HostKeyTypeOrder is modern-to-legacy, matching design-docs/HostVerb.md's own "show all key types present" decision - every type the host actually has is shown, in this fixed order, rather than picking one.

View Source
var VirtualizationContainerTechs = map[string]bool{
	"docker": true, "lxc": true, "lxd": true, "podman": true,
	"container": true, "openvz": true, "jail": true, "chroot": true, "zone": true,
}

VirtualizationContainerTechs/virtualizationVMTechs categorize ansible_facts' own "virtualization_type" (and, as a fallback, "virtualization_tech_guest" entries) into design-docs/HostVerb.md's own VM/Container/Bare Metal buckets - a documented heuristic, not an exhaustive list of every virtualization technology Ansible can report, same "good enough, not chased further" style as this project's other text-classification heuristics (e.g. TaskLabel's truncation, PrimaryOutputField's stdout-vs-msg choice). An unrecognized-but-real type falls back to showing the raw value rather than a wrong bucket.

View Source
var VirtualizationVMTechs = map[string]bool{
	"kvm": true, "qemu": true, "vmware": true, "virtualbox": true,
	"xen": true, "hyperv": true, "parallels": true, "bhyve": true, "uml": true,
}

Functions

func BuildHostDetailPrimitive

func BuildHostDetailPrimitive(app *tview.Application, stubPath, hostname, playbook string, rest []string, footerText string, pingKnown, pingOK bool) (tview.Primitive, *uikit.TabbedPane, *tview.TextView, *uikit.TabSearchBar)

BuildHostDetailPrimitive builds design-docs/HostVerb.md's five-tab host detail view - a thin header (hostname + playbook, mirroring the "template" Verb's own header pattern) and a five-tab body (TabbedPane, tabs.go) - shared unchanged between the standalone "host <name>" Verb (runHostDetailStandalone) and the "hosts" Verb's own list-then-detail flow (runHostsListTUI); the two differ only in what Esc does, which the caller wires itself via its own SetInputCapture, not this function. Returns the built Flex alongside the TabbedPane/header/footer so the caller's own input/mouse capture can drive tab-switching and swallow clicks on the header/footer bars, the same way template.go's runTemplateTUI does inline for its own, simpler two-tab view.

Every tab's own data is fetched concurrently, each on its own goroutine, starting the instant this function returns - not deferred until a tab is first viewed - per design-docs/HostVerb.md's own "Findings from discussion". The view itself (and each tab's own placeholder text) appears immediately; each goroutine updates its own tab via app.QueueUpdateDraw once its own fetch completes - the same async-update mechanism resolved.go/ansibledoc.go already use for the drill-down view's own Resolved/Docs tabs, just kicked off eagerly for every tab at once instead of lazily per tab-open. pingKnown/pingOK carry the hosts list's own already-known connectivity dot for this host, if any (false/false for the standalone "host" Verb, which has no ping subsystem at all - RunHostDetailStandalone always passes that) - see the Summary tab's own progressive-fetch goroutine below for what this changes.

func ClassifyVirtualization

func ClassifyVirtualization(facts map[string]interface{}) string

func DedupProcessorModels

func DedupProcessorModels(raw interface{}) []string

DedupProcessorModels extracts unique processor model-name strings out of ansible_facts' own "processor" field. Confirmed empirically (see host_test.go): that fact is a flat list repeating, once per logical core, a 3-element group of [core index, vendor id, model name] - e.g. ["0", "AuthenticAMD", "AMD Ryzen 5 3600 6-Core Processor", "1", "AuthenticAMD", "AMD Ryzen 5 3600 6-Core Processor", ...]. Rather than depend on that exact grouping (which could differ across platforms/ ansible-core versions), this uses a simpler, platform-independent heuristic: a real model-name string always contains a space (e.g. "AMD Ryzen 5 3600 6-Core Processor"), while a core index ("0") or vendor id ("AuthenticAMD") never does - so filtering for entries containing a space and deduplicating, preserving first-seen order, reliably yields just the distinct model names (handling the rare heterogeneous-CPU case too) without needing to know the grouping width at all.

func DiscoverHostVarsFiles

func DiscoverHostVarsFiles(hostname string, dirs []string) []string

DiscoverHostVarsFiles returns every host_vars file for hostname found under any of dirs, sorted for determinism - Ansible looks for host_vars as a sibling of both the inventory source and the playbook (ansible-core's own documented behavior), so dirs is expected to already carry both candidates by the time this is called (extractInventoryDirs plus the playbook's own directory - see fetchHostVars). Matches both shapes Ansible itself recognizes: a single host_vars/<hostname>.yml (or .yaml) file, and a host_vars/<hostname>/ directory of multiple files. Deduplicated by absolute path, since the playbook and an inventory source can easily share the same directory.

func ExtractInventoryDirs

func ExtractInventoryDirs(rest []string) []string

ExtractInventoryDirs pulls every -i/--inventory value out of rest (both "--flag value" and "--flag=value" long forms, same convention ParsePassthroughArgs uses in rerunargs.go for --tags/--limit) and returns the directory each one lives in, for any that resolve to a real file or directory on disk - silently skipping anything else (a bare comma-list like "web1,web2,", a nonexistent path) since neither has a meaningful directory to look for a sibling host_vars/ under.

func FactString

func FactString(facts map[string]interface{}, key string) string

FactString/factStringList pull a string/[]string field out of a decoded ansible_facts map, tolerating an absent or wrongly-shaped key the same way DecodeHostResult tolerates a malformed payload elsewhere - "" / nil rather than a panic or an error, since a field simply not being gathered on a given platform is normal, not exceptional. key is the short fact name (e.g. "fqdn", "distribution") - both functions add the "ansible_" prefix themselves. Confirmed empirically (a real gather_facts run, not assumed from the `debug: var: ansible_facts` shape used elsewhere in this project, which is templated/prefix-stripped and looks different): the *task result JSON* this whole file reads (`v2_runner_on_ok`'s own `hosts.<host>.ansible_facts`) nests every fact under its full "ansible_<name>" key, not the short name - e.g. "ansible_fqdn", "ansible_processor", not "fqdn"/"processor".

func FactStringList

func FactStringList(facts map[string]interface{}, key string) []string

func FetchHostEverythingKnownRaw added in v0.2.0

func FetchHostEverythingKnownRaw(hostname string, rest []string) ([]byte, error)

FetchHostEverythingKnownRaw is design-docs/HostVerb.md's own fifth tab - runAnsibleInventoryHost's own output, trimmed but not yet rendered (BuildHostDetailPrimitive handles that, via uikit.HighlightJSONKeys, both for this raw form and again for mergeFactsIntoEverythingKnown's own merged result - see that function's own doc comment for why this needed splitting out of what used to be one single fetchHostEverythingKnown). That tool's default output is already pretty-printed JSON - confirmed empirically, no reformatting needed for this raw form. So this tab's own base content is: always declared inventory data, plus whichever gathered facts happen to be sitting in the fact cache already, if any - not fetched live either way, but not guaranteed static either (see runAnsibleInventoryHost's own doc comment).

func FetchHostFacts added in v0.2.0

func FetchHostFacts(stubPath, hostname string, rest []string, skipLiveGather bool) (hostFactsResult, error)

FetchHostFacts first checks whether a fresh fact-cache entry already covers this host (runAnsibleInventoryHost, gated on factCacheCanaryKey) and uses that directly, with no connection to the host at all, when it does - restoring the original design intent ("if fact caching is activated, retrieve from there, otherwise retrieve from host") that got lost when hostSummaryStubYAML's own fix (see its doc comment) switched to an explicit `setup:` task that always connects: that fix was necessary for correctness (the play-level `gather_facts: true` shorthand could silently skip with zero jsonl output at all under `gathering = smart`), but it also meant paying for a live connection on every view, even when a perfectly fresh cache already existed. Checking the cache first, explicitly, in application code rather than leaning on ansible's own smart-gathering skip, gets the speed back without reintroducing the silent-failure bug: a cache hit here is a real, parsed, guaranteed-fresh result (runAnsibleInventoryHost's own doc comment - confirmed empirically that ansible-inventory --host stops showing a host's cached facts once fact_caching_timeout expires), never a guess.

Any problem with the cache-first check itself (the command fails, its output isn't valid JSON, or the canary key just isn't there) falls straight through to the live path below without comment - all three are ordinary, expected cases (no fact_caching configured at all is the common one), not worth surfacing as their own error when the live path is about to attempt the exact same thing anyway and will report its own error if that fails too.

skipLiveGather (design-docs/HostVerb.md's own "New ideas": a host the hosts list's own ping dot already found unreachable shouldn't pay for a second, doomed connection attempt just to open its Summary tab) only ever short-circuits the live path below, never the cache check above a stale-but-real cached fact is still worth showing even for a host that's down right now.

Falling through to the live path: stubPath runs synchronously, narrowed to hostname via --limit, and this extracts that host's own ansible_facts from the resulting jsonl stream - the same scan-for-one-host's-own-event pattern renderTemplate (template.go) already uses. err is non-nil only when nothing usable could be determined at all (a bad inventory/host); an unreachable/failed host is reported as ordinary displayable text instead, not err, since that's expected, common content for this tab, not a tool failure.

func FetchHostGroups

func FetchHostGroups(hostname string, rest []string) (string, error)

FetchHostGroups runs `ansible-inventory --list` and renders hostname's own full transitive group chain (hostGroupChain), one line per group, left-aligned to the widest group name so the "(direct)"/"(via ...)" annotations line up.

func FetchHostPing added in v0.2.0

func FetchHostPing(stubPath, hostname string, rest []string) bool

FetchHostPing runs hostPingStubYAML against hostname and reports whether it came back with a genuine v2_runner_on_ok - the host list's own connectivity dot (design-docs/HostVerb.md). Deliberately always a live attempt, never a fact-cache shortcut the way fetchHostSummary's own live path has one: a cached fact proves nothing about whether the host is reachable right now. Green-or-red only, matching that same decision - a failure, an unreachable result, and an inability to even run ansible-playbook at all (bad inventory, missing binary) all collapse to the same "false", with no separate error surfaced for this dot's own purposes (the five detail tabs already report a real error for exactly this host if there's one to see).

func FetchHostPlays

func FetchHostPlays(playbook string, rest []string, hostname string) (string, error)

FetchHostPlays runs "ansible-playbook <playbook> <rest...> --limit <hostname> --list-tasks --list-hosts" and groups the flattened ProgressEntry sequence ParseListTasksOutput (progress.go) already produces back into per-play sections, reusing that parser directly rather than reimplementing it - narrowed to exactly this host via the same --limit flag progress.go's own doc comment already explains is required alongside --list-hosts for a limit to actually apply at all. Unlike BuildProgressSkeleton (progress.go), which is always best-effort and swallows every failure silently (fine for an optional progress indicator riding on top of an already-working run), this surfaces a real failure as err, since an empty Plays tab needs to stay distinguishable from "the whole invocation failed."

func FetchHostVars

func FetchHostVars(hostname, playbook string, rest []string) (string, error)

FetchHostVars renders every host_vars file found for hostname verbatim (design-docs/HostVerb.md's own "Findings from discussion": raw file content, one section per file, not a merged key-value view - preserves comments/formatting and needs no variable-precedence logic), one SectionLabel-headed section per file, in discoverHostVarsFiles' own sorted order.

func FilterLinkLocal

func FilterLinkLocal(addrs []string) []string

FilterLinkLocal drops fe80::/10 link-local IPv6 addresses - present on essentially every interface and not generally useful for identifying a host, so they'd otherwise clutter the IPv6 summary line on any multi-interface host.

func FormatRAM

func FormatRAM(raw interface{}) string

FormatRAM renders ansible_facts' own "memtotal_mb" (always a JSON number, so always a float64 once decoded into interface{}) as "N.N GB", one decimal place.

func HostRowText

func HostRowText(hostname string, selected bool, pingKnown, pingOK bool) string

HostRowText renders one row of "hosts"'s own list - plain white text normally, or black bold text on a light gray background when selected, the same "cursor row" convention every other selectable row in this app uses (PlayRowText/TaskLabel/HostLabel's own selected parameter, tui.go). pingKnown/pingOK back design-docs/HostVerb.md's own "New ideas" connectivity dot: a green "●" once this host's own ansible.builtin.ping has come back OK, red once it's come back anything else (failed/unreachable/couldn't even run), nothing at all while that row's own ping is still in flight - lazy per-row update, no placeholder spinner, matching that decision's own "row shows its dot the moment its own ping lands" wording.

func JoinNonEmpty

func JoinNonEmpty(sep string, parts ...string) string

JoinNonEmpty joins only parts that are actually non-empty - used for "OS"/"Distribution" summary fields, each built from two separate facts that could individually be missing (e.g. ansible_kernel absent on a platform that doesn't report one).

func ParseHostArgs

func ParseHostArgs(args []string) (hostname, playbookArg string, rest []string, ok bool)

ParseHostArgs splits args (everything after the "host" Verb) into the required hostname, an optional playbook, and everything else as passthrough args - "tangsible host <hostname> [<playbook>] [-i ...] [-e ...]" (design-docs/HostVerb.md), the same shape parseTemplateArgs (template.go) already uses for "<path> [<hostname>]", just with the two positionals' roles swapped: hostname is only recognized when it's the *first* leading positional; playbook only when it's the *second*, immediately after hostname and before any flag-shaped token. ok is false if no hostname was given at all (a missing or flag-shaped first argument).

func RunAnsibleInventoryHost

func RunAnsibleInventoryHost(hostname string, rest []string) ([]byte, error)

RunAnsibleInventoryHost runs "ansible-inventory --host <hostname>" and returns its raw stdout - the one subprocess invocation shared by fetchHostEverythingKnown (shown verbatim) and fetchHostSummary's own cache-first check (parsed into a map, see below), so the command is only ever built and run in one place.

--host itself never connects to the host - it only ever reads local state (inventory/group_vars/host_vars, merged by ansible's own precedence rules, plus whatever's on disk already) - but "never connects" turned out not to mean "never shows gathered facts" the way an earlier version of this comment claimed: confirmed empirically (after a live report caught this exact discrepancy) that when `fact_caching` is configured in ansible.cfg *and* that cache already holds an entry for the host - written by any prior real gather, not necessarily one this session ran - ansible-inventory merges those cached facts straight into --host's own output too, same as it merges host_vars/group_vars, and drops them again once fact_caching_timeout expires (also confirmed empirically) - so a hit here is guaranteed fresh within whatever window the user's own ansible.cfg configures, never arbitrarily stale. fetchHostSummary uses this to skip an actual connection entirely when a fresh cache entry already exists.

func RunHostDetailStandalone

func RunHostDetailStandalone(hostname, playbook string, rest []string, stubPath string)

RunHostDetailStandalone builds and runs "tangsible host <hostname>"'s own standalone program (design-docs/HostVerb.md) - a single tview.Application showing exactly one host's detail view for the process's entire lifetime, no list to go back to. Esc is deliberately inert here, same reasoning and precedent as the "template" Verb's own view (template.go/runTemplateTUI): only q/Ctrl-C quit, so idly browsing tabs can never close the whole thing by reflex.