feat(ansible): add virtual setup facts support
Some checks are pending
CI / test (push) Waiting to run
CI / auto-fix (push) Waiting to run
CI / auto-merge (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 15:40:59 +00:00
parent d34aed9feb
commit 56d532885d
5 changed files with 116 additions and 25 deletions

View file

@ -2498,7 +2498,7 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
return err
}
facts, err := e.collectFacts(ctx, client)
facts, err := e.collectFacts(ctx, client, false)
if err != nil {
return err
}
@ -2922,6 +2922,10 @@ func (e *Executor) lookupConditionValue(name string, host string, task *Task, lo
return facts.Architecture, true
case "ansible_kernel":
return facts.Kernel, true
case "ansible_virtualization_role":
return facts.VirtualizationRole, true
case "ansible_virtualization_type":
return facts.VirtualizationType, true
}
}
@ -3215,6 +3219,10 @@ func (e *Executor) resolveExprBase(expr string, host string, task *Task) string
return facts.Architecture
case "ansible_kernel":
return facts.Kernel
case "ansible_virtualization_role":
return facts.VirtualizationRole
case "ansible_virtualization_type":
return facts.VirtualizationType
}
}

View file

@ -3885,13 +3885,16 @@ func (e *Executor) moduleSetup(ctx context.Context, host string, client sshFacts
defer cancel()
}
facts, err := e.collectFacts(ctx, client)
gatherSubset := normalizeStringList(args["gather_subset"])
includeVirtual := containsString(gatherSubset, "all") || containsString(gatherSubset, "virtual")
facts, err := e.collectFacts(ctx, client, includeVirtual)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil
}
factMap := factsToMap(facts)
factMap = applyGatherSubsetFilter(factMap, normalizeStringList(args["gather_subset"]))
factMap = applyGatherSubsetFilter(factMap, gatherSubset)
filteredFactMap := filterFactsMap(factMap, normalizeStringList(args["filter"]))
filteredFacts := factsFromMap(filteredFactMap)
@ -3906,6 +3909,15 @@ func (e *Executor) moduleSetup(ctx context.Context, host string, client sshFacts
}, nil
}
func containsString(values []string, target string) bool {
for _, value := range values {
if lower(corexTrimSpace(value)) == target {
return true
}
}
return false
}
func applyGatherSubsetFilter(facts map[string]any, subsets []string) map[string]any {
if len(facts) == 0 || len(subsets) == 0 {
return facts
@ -4049,13 +4061,16 @@ func gatherSubsetKeys(subset string) []string {
"ansible_distribution_version",
}
case "virtual":
return nil
return []string{
"ansible_virtualization_role",
"ansible_virtualization_type",
}
default:
return nil
}
}
func (e *Executor) collectFacts(ctx context.Context, client sshFactsRunner) (*Facts, error) {
func (e *Executor) collectFacts(ctx context.Context, client sshFactsRunner, includeVirtual bool) (*Facts, error) {
facts := &Facts{}
read := func(cmd string) (string, error) {
stdout, _, _, err := client.Run(ctx, cmd)
@ -4154,6 +4169,21 @@ func (e *Executor) collectFacts(ctx context.Context, client sshFactsRunner) (*Fa
facts.IPv4 = corexTrimSpace(stdout)
}
if includeVirtual {
stdout, err = read("systemd-detect-virt 2>/dev/null")
if err != nil {
return nil, err
}
virtType := corexTrimSpace(stdout)
if virtType == "" || virtType == "none" {
facts.VirtualizationRole = "host"
facts.VirtualizationType = "none"
} else {
facts.VirtualizationRole = "guest"
facts.VirtualizationType = virtType
}
}
return facts, nil
}
@ -4170,6 +4200,8 @@ func factsToMap(facts *Facts) map[string]any {
"ansible_distribution_version": facts.Version,
"ansible_architecture": facts.Architecture,
"ansible_kernel": facts.Kernel,
"ansible_virtualization_role": facts.VirtualizationRole,
"ansible_virtualization_type": facts.VirtualizationType,
"ansible_memtotal_mb": facts.Memory,
"ansible_processor_vcpus": facts.CPUs,
"ansible_default_ipv4_address": facts.IPv4,
@ -4225,6 +4257,12 @@ func factsFromMap(values map[string]any) *Facts {
if v, ok := values["ansible_kernel"].(string); ok {
facts.Kernel = v
}
if v, ok := values["ansible_virtualization_role"].(string); ok {
facts.VirtualizationRole = v
}
if v, ok := values["ansible_virtualization_type"].(string); ok {
facts.VirtualizationType = v
}
if v, ok := values["ansible_memtotal_mb"].(int64); ok {
facts.Memory = v
}

View file

@ -1071,6 +1071,45 @@ func TestModulesInfra_ModuleSetup_Good_FilteredFacts(t *testing.T) {
assert.Equal(t, "debian", e.facts["host1"].Distribution)
}
func TestModulesInfra_ModuleSetup_Good_VirtualSubset(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`hostname -f`, "web1.example.com\n", "", 0)
mock.expectCommand(`hostname -s`, "web1\n", "", 0)
mock.expectCommand(`cat /etc/os-release`, "ID=debian\nVERSION_ID=12\n", "", 0)
mock.expectCommand(`uname -m`, "x86_64\n", "", 0)
mock.expectCommand(`uname -r`, "6.1.0\n", "", 0)
mock.expectCommand(`nproc`, "8\n", "", 0)
mock.expectCommand(`free -m`, "16384\n", "", 0)
mock.expectCommand(`hostname -I`, "10.0.0.11\n", "", 0)
mock.expectCommand(`systemd-detect-virt`, "docker\n", "", 0)
task := &Task{
Module: "setup",
Args: map[string]any{
"gather_subset": "!all,!min,virtual",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
require.NotNil(t, result)
assert.False(t, result.Changed)
facts, ok := result.Data["ansible_facts"].(map[string]any)
require.True(t, ok)
assert.Len(t, facts, 2)
assert.Equal(t, "guest", facts["ansible_virtualization_role"])
assert.Equal(t, "docker", facts["ansible_virtualization_type"])
assert.NotContains(t, facts, "ansible_hostname")
require.NotNil(t, e.facts["host1"])
assert.Equal(t, "guest", e.facts["host1"].VirtualizationRole)
assert.Equal(t, "docker", e.facts["host1"].VirtualizationType)
assert.Equal(t, "guest", e.templateString("{{ ansible_virtualization_role }}", "host1", nil))
assert.Equal(t, "docker", e.templateString("{{ ansible_virtualization_type }}", "host1", nil))
}
func TestModulesInfra_ModuleSetup_Good_GatherSubset(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")

View file

@ -369,16 +369,18 @@ type Host struct {
//
// facts := Facts{Hostname: "web1", Distribution: "Ubuntu", Kernel: "Linux"}
type Facts struct {
Hostname string `json:"ansible_hostname"`
FQDN string `json:"ansible_fqdn"`
OS string `json:"ansible_os_family"`
Distribution string `json:"ansible_distribution"`
Version string `json:"ansible_distribution_version"`
Architecture string `json:"ansible_architecture"`
Kernel string `json:"ansible_kernel"`
Memory int64 `json:"ansible_memtotal_mb"`
CPUs int `json:"ansible_processor_vcpus"`
IPv4 string `json:"ansible_default_ipv4_address"`
Hostname string `json:"ansible_hostname"`
FQDN string `json:"ansible_fqdn"`
OS string `json:"ansible_os_family"`
Distribution string `json:"ansible_distribution"`
Version string `json:"ansible_distribution_version"`
Architecture string `json:"ansible_architecture"`
Kernel string `json:"ansible_kernel"`
VirtualizationRole string `json:"ansible_virtualization_role"`
VirtualizationType string `json:"ansible_virtualization_type"`
Memory int64 `json:"ansible_memtotal_mb"`
CPUs int `json:"ansible_processor_vcpus"`
IPv4 string `json:"ansible_default_ipv4_address"`
}
// KnownModules lists the Ansible module names recognized by the parser.

View file

@ -911,22 +911,26 @@ all:
func TestTypes_Facts_Good_Struct(t *testing.T) {
facts := Facts{
Hostname: "web1",
FQDN: "web1.example.com",
OS: "Debian",
Distribution: "ubuntu",
Version: "24.04",
Architecture: "x86_64",
Kernel: "6.8.0",
Memory: 16384,
CPUs: 4,
IPv4: "10.0.0.1",
Hostname: "web1",
FQDN: "web1.example.com",
OS: "Debian",
Distribution: "ubuntu",
Version: "24.04",
Architecture: "x86_64",
Kernel: "6.8.0",
VirtualizationRole: "guest",
VirtualizationType: "docker",
Memory: 16384,
CPUs: 4,
IPv4: "10.0.0.1",
}
assert.Equal(t, "web1", facts.Hostname)
assert.Equal(t, "web1.example.com", facts.FQDN)
assert.Equal(t, "ubuntu", facts.Distribution)
assert.Equal(t, "x86_64", facts.Architecture)
assert.Equal(t, "guest", facts.VirtualizationRole)
assert.Equal(t, "docker", facts.VirtualizationType)
assert.Equal(t, int64(16384), facts.Memory)
assert.Equal(t, 4, facts.CPUs)
}