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 return err
} }
facts, err := e.collectFacts(ctx, client) facts, err := e.collectFacts(ctx, client, false)
if err != nil { if err != nil {
return err return err
} }
@ -2922,6 +2922,10 @@ func (e *Executor) lookupConditionValue(name string, host string, task *Task, lo
return facts.Architecture, true return facts.Architecture, true
case "ansible_kernel": case "ansible_kernel":
return facts.Kernel, true 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 return facts.Architecture
case "ansible_kernel": case "ansible_kernel":
return facts.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() 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 { if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil return &TaskResult{Failed: true, Msg: err.Error()}, nil
} }
factMap := factsToMap(facts) factMap := factsToMap(facts)
factMap = applyGatherSubsetFilter(factMap, normalizeStringList(args["gather_subset"])) factMap = applyGatherSubsetFilter(factMap, gatherSubset)
filteredFactMap := filterFactsMap(factMap, normalizeStringList(args["filter"])) filteredFactMap := filterFactsMap(factMap, normalizeStringList(args["filter"]))
filteredFacts := factsFromMap(filteredFactMap) filteredFacts := factsFromMap(filteredFactMap)
@ -3906,6 +3909,15 @@ func (e *Executor) moduleSetup(ctx context.Context, host string, client sshFacts
}, nil }, 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 { func applyGatherSubsetFilter(facts map[string]any, subsets []string) map[string]any {
if len(facts) == 0 || len(subsets) == 0 { if len(facts) == 0 || len(subsets) == 0 {
return facts return facts
@ -4049,13 +4061,16 @@ func gatherSubsetKeys(subset string) []string {
"ansible_distribution_version", "ansible_distribution_version",
} }
case "virtual": case "virtual":
return nil return []string{
"ansible_virtualization_role",
"ansible_virtualization_type",
}
default: default:
return nil 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{} facts := &Facts{}
read := func(cmd string) (string, error) { read := func(cmd string) (string, error) {
stdout, _, _, err := client.Run(ctx, cmd) stdout, _, _, err := client.Run(ctx, cmd)
@ -4154,6 +4169,21 @@ func (e *Executor) collectFacts(ctx context.Context, client sshFactsRunner) (*Fa
facts.IPv4 = corexTrimSpace(stdout) 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 return facts, nil
} }
@ -4170,6 +4200,8 @@ func factsToMap(facts *Facts) map[string]any {
"ansible_distribution_version": facts.Version, "ansible_distribution_version": facts.Version,
"ansible_architecture": facts.Architecture, "ansible_architecture": facts.Architecture,
"ansible_kernel": facts.Kernel, "ansible_kernel": facts.Kernel,
"ansible_virtualization_role": facts.VirtualizationRole,
"ansible_virtualization_type": facts.VirtualizationType,
"ansible_memtotal_mb": facts.Memory, "ansible_memtotal_mb": facts.Memory,
"ansible_processor_vcpus": facts.CPUs, "ansible_processor_vcpus": facts.CPUs,
"ansible_default_ipv4_address": facts.IPv4, "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 { if v, ok := values["ansible_kernel"].(string); ok {
facts.Kernel = v 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 { if v, ok := values["ansible_memtotal_mb"].(int64); ok {
facts.Memory = v 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) 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) { func TestModulesInfra_ModuleSetup_Good_GatherSubset(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")

View file

@ -369,16 +369,18 @@ type Host struct {
// //
// facts := Facts{Hostname: "web1", Distribution: "Ubuntu", Kernel: "Linux"} // facts := Facts{Hostname: "web1", Distribution: "Ubuntu", Kernel: "Linux"}
type Facts struct { type Facts struct {
Hostname string `json:"ansible_hostname"` Hostname string `json:"ansible_hostname"`
FQDN string `json:"ansible_fqdn"` FQDN string `json:"ansible_fqdn"`
OS string `json:"ansible_os_family"` OS string `json:"ansible_os_family"`
Distribution string `json:"ansible_distribution"` Distribution string `json:"ansible_distribution"`
Version string `json:"ansible_distribution_version"` Version string `json:"ansible_distribution_version"`
Architecture string `json:"ansible_architecture"` Architecture string `json:"ansible_architecture"`
Kernel string `json:"ansible_kernel"` Kernel string `json:"ansible_kernel"`
Memory int64 `json:"ansible_memtotal_mb"` VirtualizationRole string `json:"ansible_virtualization_role"`
CPUs int `json:"ansible_processor_vcpus"` VirtualizationType string `json:"ansible_virtualization_type"`
IPv4 string `json:"ansible_default_ipv4_address"` 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. // 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) { func TestTypes_Facts_Good_Struct(t *testing.T) {
facts := Facts{ facts := Facts{
Hostname: "web1", Hostname: "web1",
FQDN: "web1.example.com", FQDN: "web1.example.com",
OS: "Debian", OS: "Debian",
Distribution: "ubuntu", Distribution: "ubuntu",
Version: "24.04", Version: "24.04",
Architecture: "x86_64", Architecture: "x86_64",
Kernel: "6.8.0", Kernel: "6.8.0",
Memory: 16384, VirtualizationRole: "guest",
CPUs: 4, VirtualizationType: "docker",
IPv4: "10.0.0.1", Memory: 16384,
CPUs: 4,
IPv4: "10.0.0.1",
} }
assert.Equal(t, "web1", facts.Hostname) assert.Equal(t, "web1", facts.Hostname)
assert.Equal(t, "web1.example.com", facts.FQDN) assert.Equal(t, "web1.example.com", facts.FQDN)
assert.Equal(t, "ubuntu", facts.Distribution) assert.Equal(t, "ubuntu", facts.Distribution)
assert.Equal(t, "x86_64", facts.Architecture) 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, int64(16384), facts.Memory)
assert.Equal(t, 4, facts.CPUs) assert.Equal(t, 4, facts.CPUs)
} }