Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/ansible/RFC.md fully. Find ONE feat...' (#34) from main into dev
This commit is contained in:
commit
b0c95244be
3 changed files with 131 additions and 39 deletions
111
executor.go
111
executor.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
|
@ -44,6 +45,10 @@ type Executor struct {
|
|||
Verbose int
|
||||
}
|
||||
|
||||
type commandRunner interface {
|
||||
Run(ctx context.Context, cmd string) (stdout, stderr string, exitCode int, err error)
|
||||
}
|
||||
|
||||
// NewExecutor creates a new playbook executor.
|
||||
//
|
||||
// Example:
|
||||
|
|
@ -905,7 +910,16 @@ func (e *Executor) getClient(host string, play *Play) (*SSHClient, error) {
|
|||
|
||||
// gatherFacts collects facts from a host.
|
||||
func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) error {
|
||||
if play.Connection == "local" || host == "localhost" {
|
||||
client, err := e.getClient(host, play)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.populateFacts(ctx, host, play, client)
|
||||
}
|
||||
|
||||
func (e *Executor) populateFacts(ctx context.Context, host string, play *Play, client commandRunner) error {
|
||||
if (play != nil && play.Connection == "local") || host == "localhost" {
|
||||
// Local facts
|
||||
e.facts[host] = &Facts{
|
||||
Hostname: "localhost",
|
||||
|
|
@ -913,11 +927,6 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
|
|||
return nil
|
||||
}
|
||||
|
||||
client, err := e.getClient(host, play)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Gather basic facts
|
||||
facts := &Facts{}
|
||||
|
||||
|
|
@ -938,10 +947,16 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
|
|||
if corexHasPrefix(line, "ID=") {
|
||||
facts.Distribution = trimCutset(corexTrimPrefix(line, "ID="), "\"")
|
||||
}
|
||||
if corexHasPrefix(line, "ID_LIKE=") && facts.OS == "" {
|
||||
facts.OS = titleFactFamily(trimCutset(corexTrimPrefix(line, "ID_LIKE="), "\""))
|
||||
}
|
||||
if corexHasPrefix(line, "VERSION_ID=") {
|
||||
facts.Version = trimCutset(corexTrimPrefix(line, "VERSION_ID="), "\"")
|
||||
}
|
||||
}
|
||||
if facts.OS == "" {
|
||||
facts.OS = titleFactFamily(facts.Distribution)
|
||||
}
|
||||
|
||||
// Architecture
|
||||
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
||||
|
|
@ -951,6 +966,16 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
|
|||
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
||||
facts.Kernel = corexTrimSpace(stdout)
|
||||
|
||||
// Memory, CPU count, and primary IPv4 address
|
||||
stdout, _, _, _ = client.Run(ctx, "awk '/MemTotal:/ { print int($2 / 1024) }' /proc/meminfo")
|
||||
facts.Memory = parseFactInt64(stdout)
|
||||
|
||||
stdout, _, _, _ = client.Run(ctx, "nproc --all 2>/dev/null || getconf _NPROCESSORS_ONLN")
|
||||
facts.CPUs = int(parseFactInt64(stdout))
|
||||
|
||||
stdout, _, _, _ = client.Run(ctx, "hostname -I 2>/dev/null | awk '{ print $1 }'")
|
||||
facts.IPv4 = firstNonEmptyField(stdout)
|
||||
|
||||
e.mu.Lock()
|
||||
e.facts[host] = facts
|
||||
e.mu.Unlock()
|
||||
|
|
@ -990,6 +1015,60 @@ func normalizeConditions(when any) []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseFactInt64(stdout string) int64 {
|
||||
fields := fields(stdout)
|
||||
if len(fields) == 0 {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func firstNonEmptyField(stdout string) string {
|
||||
for _, field := range fields(stdout) {
|
||||
if field != "" {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func titleFactFamily(value string) string {
|
||||
value = corexTrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
fields := fields(value)
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
family := lower(fields[0])
|
||||
switch family {
|
||||
case "debian", "ubuntu", "linuxmint":
|
||||
return "Debian"
|
||||
case "rhel", "redhat", "centos", "fedora", "rocky", "almalinux", "alma", "ol", "oracle":
|
||||
return "RedHat"
|
||||
case "sles", "suse", "opensuse":
|
||||
return "Suse"
|
||||
case "arch", "manjaro":
|
||||
return "Archlinux"
|
||||
case "alpine":
|
||||
return "Alpine"
|
||||
}
|
||||
|
||||
runes := []rune(family)
|
||||
if len(runes) == 0 {
|
||||
return ""
|
||||
}
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// evalCondition evaluates a single condition.
|
||||
func (e *Executor) evalCondition(cond string, host string) bool {
|
||||
cond = corexTrimSpace(cond)
|
||||
|
|
@ -1119,6 +1198,10 @@ func (e *Executor) lookupTemplateValue(name string, host string, task *Task) (an
|
|||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Version, true
|
||||
}
|
||||
case "ansible_os_family":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.OS, true
|
||||
}
|
||||
case "ansible_architecture":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Architecture, true
|
||||
|
|
@ -1127,6 +1210,18 @@ func (e *Executor) lookupTemplateValue(name string, host string, task *Task) (an
|
|||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Kernel, true
|
||||
}
|
||||
case "ansible_memtotal_mb":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.Memory, true
|
||||
}
|
||||
case "ansible_processor_vcpus":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.CPUs, true
|
||||
}
|
||||
case "ansible_default_ipv4_address":
|
||||
if facts, ok := e.facts[host]; ok {
|
||||
return facts.IPv4, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
|
|
@ -1417,10 +1512,14 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) {
|
|||
if facts, ok := e.facts[host]; ok {
|
||||
context["ansible_hostname"] = facts.Hostname
|
||||
context["ansible_fqdn"] = facts.FQDN
|
||||
context["ansible_os_family"] = facts.OS
|
||||
context["ansible_distribution"] = facts.Distribution
|
||||
context["ansible_distribution_version"] = facts.Version
|
||||
context["ansible_architecture"] = facts.Architecture
|
||||
context["ansible_kernel"] = facts.Kernel
|
||||
context["ansible_memtotal_mb"] = facts.Memory
|
||||
context["ansible_processor_vcpus"] = facts.CPUs
|
||||
context["ansible_default_ipv4_address"] = facts.IPv4
|
||||
}
|
||||
|
||||
buf := newBuilder()
|
||||
|
|
|
|||
|
|
@ -410,18 +410,26 @@ func TestExecutorExtra_ResolveExpr_Good_Facts(t *testing.T) {
|
|||
e.facts["host1"] = &Facts{
|
||||
Hostname: "web01",
|
||||
FQDN: "web01.example.com",
|
||||
OS: "Debian",
|
||||
Distribution: "ubuntu",
|
||||
Version: "22.04",
|
||||
Architecture: "x86_64",
|
||||
Kernel: "5.15.0",
|
||||
Memory: 32768,
|
||||
CPUs: 16,
|
||||
IPv4: "10.0.0.10",
|
||||
}
|
||||
|
||||
assert.Equal(t, "web01", e.resolveExpr("ansible_hostname", "host1", nil))
|
||||
assert.Equal(t, "web01.example.com", e.resolveExpr("ansible_fqdn", "host1", nil))
|
||||
assert.Equal(t, "Debian", e.resolveExpr("ansible_os_family", "host1", nil))
|
||||
assert.Equal(t, "ubuntu", e.resolveExpr("ansible_distribution", "host1", nil))
|
||||
assert.Equal(t, "22.04", e.resolveExpr("ansible_distribution_version", "host1", nil))
|
||||
assert.Equal(t, "x86_64", e.resolveExpr("ansible_architecture", "host1", nil))
|
||||
assert.Equal(t, "5.15.0", e.resolveExpr("ansible_kernel", "host1", nil))
|
||||
assert.Equal(t, "32768", e.resolveExpr("ansible_memtotal_mb", "host1", nil))
|
||||
assert.Equal(t, "16", e.resolveExpr("ansible_processor_vcpus", "host1", nil))
|
||||
assert.Equal(t, "10.0.0.10", e.resolveExpr("ansible_default_ipv4_address", "host1", nil))
|
||||
}
|
||||
|
||||
// --- applyFilter additional coverage ---
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ansible
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -812,51 +813,35 @@ func TestModulesInfra_Facts_Good_UbuntuParsing(t *testing.T) {
|
|||
// Mock os-release output for Ubuntu
|
||||
mock.expectCommand(`hostname -f`, "web1.example.com\n", "", 0)
|
||||
mock.expectCommand(`hostname -s`, "web1\n", "", 0)
|
||||
mock.expectCommand(`cat /etc/os-release`, "ID=ubuntu\nVERSION_ID=\"24.04\"\n", "", 0)
|
||||
mock.expectCommand(`cat /etc/os-release`, "ID=ubuntu\nVERSION_ID=\"24.04\"\nID_LIKE=debian\n", "", 0)
|
||||
mock.expectCommand(`uname -m`, "x86_64\n", "", 0)
|
||||
mock.expectCommand(`uname -r`, "6.5.0-44-generic\n", "", 0)
|
||||
mock.expectCommand(`MemTotal`, "16384\n", "", 0)
|
||||
mock.expectCommand(`nproc`, "8\n", "", 0)
|
||||
mock.expectCommand(`hostname -I`, "10.0.0.1 10.0.0.2\n", "", 0)
|
||||
|
||||
// Simulate fact gathering by directly populating facts
|
||||
// using the same parsing logic as gatherFacts
|
||||
facts := &Facts{}
|
||||
|
||||
stdout, _, _, _ := mock.Run(nil, "hostname -f 2>/dev/null || hostname")
|
||||
facts.FQDN = trimFactSpace(stdout)
|
||||
|
||||
stdout, _, _, _ = mock.Run(nil, "hostname -s 2>/dev/null || hostname")
|
||||
facts.Hostname = trimFactSpace(stdout)
|
||||
|
||||
stdout, _, _, _ = mock.Run(nil, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2")
|
||||
for _, line := range splitLines(stdout) {
|
||||
if hasPrefix(line, "ID=") {
|
||||
facts.Distribution = trimQuotes(trimPrefix(line, "ID="))
|
||||
}
|
||||
if hasPrefix(line, "VERSION_ID=") {
|
||||
facts.Version = trimQuotes(trimPrefix(line, "VERSION_ID="))
|
||||
}
|
||||
}
|
||||
|
||||
stdout, _, _, _ = mock.Run(nil, "uname -m")
|
||||
facts.Architecture = trimFactSpace(stdout)
|
||||
|
||||
stdout, _, _, _ = mock.Run(nil, "uname -r")
|
||||
facts.Kernel = trimFactSpace(stdout)
|
||||
|
||||
e.facts["web1"] = facts
|
||||
require.NoError(t, e.populateFacts(context.Background(), "web1", &Play{}, mock))
|
||||
|
||||
facts := e.facts["web1"]
|
||||
require.NotNil(t, facts)
|
||||
assert.Equal(t, "web1.example.com", facts.FQDN)
|
||||
assert.Equal(t, "web1", facts.Hostname)
|
||||
assert.Equal(t, "Debian", facts.OS)
|
||||
assert.Equal(t, "ubuntu", facts.Distribution)
|
||||
assert.Equal(t, "24.04", facts.Version)
|
||||
assert.Equal(t, "x86_64", facts.Architecture)
|
||||
assert.Equal(t, "6.5.0-44-generic", facts.Kernel)
|
||||
assert.Equal(t, int64(16384), facts.Memory)
|
||||
assert.Equal(t, 8, facts.CPUs)
|
||||
assert.Equal(t, "10.0.0.1", facts.IPv4)
|
||||
|
||||
// Now verify template resolution with these facts
|
||||
result := e.templateString("{{ ansible_hostname }}", "web1", nil)
|
||||
assert.Equal(t, "web1", result)
|
||||
|
||||
result = e.templateString("{{ ansible_distribution }}", "web1", nil)
|
||||
assert.Equal(t, "ubuntu", result)
|
||||
assert.Equal(t, "web1", e.templateString("{{ ansible_hostname }}", "web1", nil))
|
||||
assert.Equal(t, "ubuntu", e.templateString("{{ ansible_distribution }}", "web1", nil))
|
||||
assert.Equal(t, "Debian", e.templateString("{{ ansible_os_family }}", "web1", nil))
|
||||
assert.Equal(t, "16384", e.templateString("{{ ansible_memtotal_mb }}", "web1", nil))
|
||||
assert.Equal(t, "8", e.templateString("{{ ansible_processor_vcpus }}", "web1", nil))
|
||||
assert.Equal(t, "10.0.0.1", e.templateString("{{ ansible_default_ipv4_address }}", "web1", nil))
|
||||
}
|
||||
|
||||
func TestModulesInfra_Facts_Good_CentOSParsing(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue