From 56d532885d5fd4deb3feea8123568161d860848f Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 15:40:59 +0000 Subject: [PATCH] feat(ansible): add virtual setup facts support Co-Authored-By: Virgil --- executor.go | 10 +++++++++- modules.go | 46 +++++++++++++++++++++++++++++++++++++++---- modules_infra_test.go | 39 ++++++++++++++++++++++++++++++++++++ types.go | 22 +++++++++++---------- types_test.go | 24 ++++++++++++---------- 5 files changed, 116 insertions(+), 25 deletions(-) diff --git a/executor.go b/executor.go index dcd9abb..26b49cc 100644 --- a/executor.go +++ b/executor.go @@ -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 } } diff --git a/modules.go b/modules.go index 0e49a14..1a144d1 100644 --- a/modules.go +++ b/modules.go @@ -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 } diff --git a/modules_infra_test.go b/modules_infra_test.go index 05453c8..ff835fd 100644 --- a/modules_infra_test.go +++ b/modules_infra_test.go @@ -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") diff --git a/types.go b/types.go index 25d578e..321eb2d 100644 --- a/types.go +++ b/types.go @@ -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. diff --git a/types_test.go b/types_test.go index 9dbe39c..f189f0d 100644 --- a/types_test.go +++ b/types_test.go @@ -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) }