refactor(ansible): upgrade core to v0.8.0-alpha.1
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
55a0f4fcfb
commit
4f33c15d6c
10 changed files with 739 additions and 299 deletions
|
|
@ -2,9 +2,6 @@ package anscmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dappco.re/go/core"
|
"dappco.re/go/core"
|
||||||
|
|
@ -16,7 +13,7 @@ import (
|
||||||
// args extracts all positional arguments from Options.
|
// args extracts all positional arguments from Options.
|
||||||
func args(opts core.Options) []string {
|
func args(opts core.Options) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, o := range opts {
|
for _, o := range opts.Items() {
|
||||||
if o.Key == "_arg" {
|
if o.Key == "_arg" {
|
||||||
if s, ok := o.Value.(string); ok {
|
if s, ok := o.Value.(string); ok {
|
||||||
out = append(out, s)
|
out = append(out, s)
|
||||||
|
|
@ -34,16 +31,16 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
playbookPath := positional[0]
|
playbookPath := positional[0]
|
||||||
|
|
||||||
// Resolve playbook path
|
// Resolve playbook path
|
||||||
if !filepath.IsAbs(playbookPath) {
|
if !pathIsAbs(playbookPath) {
|
||||||
playbookPath, _ = filepath.Abs(playbookPath)
|
playbookPath = absPath(playbookPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !coreio.Local.Exists(playbookPath) {
|
if !coreio.Local.Exists(playbookPath) {
|
||||||
return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("playbook not found: %s", playbookPath), nil)}
|
return core.Result{Value: coreerr.E("runAnsible", sprintf("playbook not found: %s", playbookPath), nil)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create executor
|
// Create executor
|
||||||
basePath := filepath.Dir(playbookPath)
|
basePath := pathDir(playbookPath)
|
||||||
executor := ansible.NewExecutor(basePath)
|
executor := ansible.NewExecutor(basePath)
|
||||||
defer executor.Close()
|
defer executor.Close()
|
||||||
|
|
||||||
|
|
@ -53,16 +50,16 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
executor.Verbose = opts.Int("verbose")
|
executor.Verbose = opts.Int("verbose")
|
||||||
|
|
||||||
if tags := opts.String("tags"); tags != "" {
|
if tags := opts.String("tags"); tags != "" {
|
||||||
executor.Tags = strings.Split(tags, ",")
|
executor.Tags = split(tags, ",")
|
||||||
}
|
}
|
||||||
if skipTags := opts.String("skip-tags"); skipTags != "" {
|
if skipTags := opts.String("skip-tags"); skipTags != "" {
|
||||||
executor.SkipTags = strings.Split(skipTags, ",")
|
executor.SkipTags = split(skipTags, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse extra vars
|
// Parse extra vars
|
||||||
if extraVars := opts.String("extra-vars"); extraVars != "" {
|
if extraVars := opts.String("extra-vars"); extraVars != "" {
|
||||||
for _, v := range strings.Split(extraVars, ",") {
|
for _, v := range split(extraVars, ",") {
|
||||||
parts := strings.SplitN(v, "=", 2)
|
parts := splitN(v, "=", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
executor.SetVar(parts[0], parts[1])
|
executor.SetVar(parts[0], parts[1])
|
||||||
}
|
}
|
||||||
|
|
@ -71,17 +68,17 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
|
|
||||||
// Load inventory
|
// Load inventory
|
||||||
if invPath := opts.String("inventory"); invPath != "" {
|
if invPath := opts.String("inventory"); invPath != "" {
|
||||||
if !filepath.IsAbs(invPath) {
|
if !pathIsAbs(invPath) {
|
||||||
invPath, _ = filepath.Abs(invPath)
|
invPath = absPath(invPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !coreio.Local.Exists(invPath) {
|
if !coreio.Local.Exists(invPath) {
|
||||||
return core.Result{Value: coreerr.E("runAnsible", fmt.Sprintf("inventory not found: %s", invPath), nil)}
|
return core.Result{Value: coreerr.E("runAnsible", sprintf("inventory not found: %s", invPath), nil)}
|
||||||
}
|
}
|
||||||
|
|
||||||
if coreio.Local.IsDir(invPath) {
|
if coreio.Local.IsDir(invPath) {
|
||||||
for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} {
|
for _, name := range []string{"inventory.yml", "hosts.yml", "inventory.yaml", "hosts.yaml"} {
|
||||||
p := filepath.Join(invPath, name)
|
p := joinPath(invPath, name)
|
||||||
if coreio.Local.Exists(p) {
|
if coreio.Local.Exists(p) {
|
||||||
invPath = p
|
invPath = p
|
||||||
break
|
break
|
||||||
|
|
@ -96,8 +93,9 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
|
|
||||||
// Set up callbacks
|
// Set up callbacks
|
||||||
executor.OnPlayStart = func(play *ansible.Play) {
|
executor.OnPlayStart = func(play *ansible.Play) {
|
||||||
fmt.Printf("\nPLAY [%s]\n", play.Name)
|
print("")
|
||||||
fmt.Println(strings.Repeat("*", 70))
|
print("PLAY [%s]", play.Name)
|
||||||
|
print("%s", repeat("*", 70))
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.OnTaskStart = func(host string, task *ansible.Task) {
|
executor.OnTaskStart = func(host string, task *ansible.Task) {
|
||||||
|
|
@ -105,9 +103,10 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
if taskName == "" {
|
if taskName == "" {
|
||||||
taskName = task.Module
|
taskName = task.Module
|
||||||
}
|
}
|
||||||
fmt.Printf("\nTASK [%s]\n", taskName)
|
print("")
|
||||||
|
print("TASK [%s]", taskName)
|
||||||
if executor.Verbose > 0 {
|
if executor.Verbose > 0 {
|
||||||
fmt.Printf("host: %s\n", host)
|
print("host: %s", host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,41 +120,42 @@ func runAnsible(opts core.Options) core.Result {
|
||||||
status = "changed"
|
status = "changed"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s: [%s]", status, host)
|
line := sprintf("%s: [%s]", status, host)
|
||||||
if result.Msg != "" && executor.Verbose > 0 {
|
if result.Msg != "" && executor.Verbose > 0 {
|
||||||
fmt.Printf(" => %s", result.Msg)
|
line = sprintf("%s => %s", line, result.Msg)
|
||||||
}
|
}
|
||||||
if result.Duration > 0 && executor.Verbose > 1 {
|
if result.Duration > 0 && executor.Verbose > 1 {
|
||||||
fmt.Printf(" (%s)", result.Duration.Round(time.Millisecond))
|
line = sprintf("%s (%s)", line, result.Duration.Round(time.Millisecond))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
print("%s", line)
|
||||||
|
|
||||||
if result.Failed && result.Stderr != "" {
|
if result.Failed && result.Stderr != "" {
|
||||||
fmt.Printf("%s\n", result.Stderr)
|
print("%s", result.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.Verbose > 1 {
|
if executor.Verbose > 1 {
|
||||||
if result.Stdout != "" {
|
if result.Stdout != "" {
|
||||||
fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout))
|
print("stdout: %s", trimSpace(result.Stdout))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.OnPlayEnd = func(play *ansible.Play) {
|
executor.OnPlayEnd = func(play *ansible.Play) {
|
||||||
fmt.Println()
|
print("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run playbook
|
// Run playbook
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
fmt.Printf("Running playbook: %s\n", playbookPath)
|
print("Running playbook: %s", playbookPath)
|
||||||
|
|
||||||
if err := executor.Run(ctx, playbookPath); err != nil {
|
if err := executor.Run(ctx, playbookPath); err != nil {
|
||||||
return core.Result{Value: coreerr.E("runAnsible", "playbook failed", err)}
|
return core.Result{Value: coreerr.E("runAnsible", "playbook failed", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nPlaybook completed in %s\n", time.Since(start).Round(time.Millisecond))
|
print("")
|
||||||
|
print("Playbook completed in %s", time.Since(start).Round(time.Millisecond))
|
||||||
|
|
||||||
return core.Result{OK: true}
|
return core.Result{OK: true}
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +167,7 @@ func runAnsibleTest(opts core.Options) core.Result {
|
||||||
}
|
}
|
||||||
host := positional[0]
|
host := positional[0]
|
||||||
|
|
||||||
fmt.Printf("Testing SSH connection to %s...\n", host)
|
print("Testing SSH connection to %s...", host)
|
||||||
|
|
||||||
cfg := ansible.SSHConfig{
|
cfg := ansible.SSHConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
|
|
@ -194,46 +194,48 @@ func runAnsibleTest(opts core.Options) core.Result {
|
||||||
}
|
}
|
||||||
connectTime := time.Since(start)
|
connectTime := time.Since(start)
|
||||||
|
|
||||||
fmt.Printf("Connected in %s\n", connectTime.Round(time.Millisecond))
|
print("Connected in %s", connectTime.Round(time.Millisecond))
|
||||||
|
|
||||||
// Gather facts
|
// Gather facts
|
||||||
fmt.Println("\nGathering facts...")
|
print("")
|
||||||
|
print("Gathering facts...")
|
||||||
|
|
||||||
stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
|
stdout, _, _, _ := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
|
||||||
fmt.Printf(" Hostname: %s\n", strings.TrimSpace(stdout))
|
print(" Hostname: %s", trimSpace(stdout))
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2")
|
stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2")
|
||||||
if stdout != "" {
|
if stdout != "" {
|
||||||
fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout))
|
print(" OS: %s", trimSpace(stdout))
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
||||||
fmt.Printf(" Kernel: %s\n", strings.TrimSpace(stdout))
|
print(" Kernel: %s", trimSpace(stdout))
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
||||||
fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout))
|
print(" Architecture: %s", trimSpace(stdout))
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'")
|
stdout, _, _, _ = client.Run(ctx, "free -h | grep Mem | awk '{print $2}'")
|
||||||
fmt.Printf(" Memory: %s\n", strings.TrimSpace(stdout))
|
print(" Memory: %s", trimSpace(stdout))
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'")
|
stdout, _, _, _ = client.Run(ctx, "df -h / | tail -1 | awk '{print $2 \" total, \" $4 \" available\"}'")
|
||||||
fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout))
|
print(" Disk: %s", trimSpace(stdout))
|
||||||
|
|
||||||
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null")
|
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf(" Docker: %s\n", strings.TrimSpace(stdout))
|
print(" Docker: %s", trimSpace(stdout))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Docker: not installed\n")
|
print(" Docker: not installed")
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, _, _, _ = client.Run(ctx, "docker ps 2>/dev/null | grep -q coolify && echo 'running' || echo 'not running'")
|
stdout, _, _, _ = client.Run(ctx, "docker ps 2>/dev/null | grep -q coolify && echo 'running' || echo 'not running'")
|
||||||
if strings.TrimSpace(stdout) == "running" {
|
if trimSpace(stdout) == "running" {
|
||||||
fmt.Printf(" Coolify: running\n")
|
print(" Coolify: running")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Coolify: not installed\n")
|
print(" Coolify: not installed")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nSSH test passed\n")
|
print("")
|
||||||
|
print("SSH test passed")
|
||||||
|
|
||||||
return core.Result{OK: true}
|
return core.Result{OK: true}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,25 @@ func Register(c *core.Core) {
|
||||||
c.Command("ansible", core.Command{
|
c.Command("ansible", core.Command{
|
||||||
Description: "Run Ansible playbooks natively (no Python required)",
|
Description: "Run Ansible playbooks natively (no Python required)",
|
||||||
Action: runAnsible,
|
Action: runAnsible,
|
||||||
Flags: core.Options{
|
Flags: core.NewOptions(
|
||||||
{Key: "inventory", Value: ""},
|
core.Option{Key: "inventory", Value: ""},
|
||||||
{Key: "limit", Value: ""},
|
core.Option{Key: "limit", Value: ""},
|
||||||
{Key: "tags", Value: ""},
|
core.Option{Key: "tags", Value: ""},
|
||||||
{Key: "skip-tags", Value: ""},
|
core.Option{Key: "skip-tags", Value: ""},
|
||||||
{Key: "extra-vars", Value: ""},
|
core.Option{Key: "extra-vars", Value: ""},
|
||||||
{Key: "verbose", Value: 0},
|
core.Option{Key: "verbose", Value: 0},
|
||||||
{Key: "check", Value: false},
|
core.Option{Key: "check", Value: false},
|
||||||
},
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Command("ansible/test", core.Command{
|
c.Command("ansible/test", core.Command{
|
||||||
Description: "Test SSH connectivity to a host",
|
Description: "Test SSH connectivity to a host",
|
||||||
Action: runAnsibleTest,
|
Action: runAnsibleTest,
|
||||||
Flags: core.Options{
|
Flags: core.NewOptions(
|
||||||
{Key: "user", Value: "root"},
|
core.Option{Key: "user", Value: "root"},
|
||||||
{Key: "password", Value: ""},
|
core.Option{Key: "password", Value: ""},
|
||||||
{Key: "key", Value: ""},
|
core.Option{Key: "key", Value: ""},
|
||||||
{Key: "port", Value: 22},
|
core.Option{Key: "port", Value: 22},
|
||||||
},
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
cmd/ansible/core_primitives.go
Normal file
160
cmd/ansible/core_primitives.go
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
package anscmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func absPath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return core.Env("DIR_CWD")
|
||||||
|
}
|
||||||
|
if core.PathIsAbs(path) {
|
||||||
|
return cleanPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd := core.Env("DIR_CWD")
|
||||||
|
if cwd == "" {
|
||||||
|
cwd = "."
|
||||||
|
}
|
||||||
|
return joinPath(cwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinPath(parts ...string) string {
|
||||||
|
ds := dirSep()
|
||||||
|
path := ""
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = part
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path = core.TrimSuffix(path, ds)
|
||||||
|
part = core.TrimPrefix(part, ds)
|
||||||
|
path = core.Concat(path, ds, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return core.CleanPath(path, ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return core.CleanPath(path, dirSep())
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathDir(path string) string {
|
||||||
|
return core.PathDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathIsAbs(path string) bool {
|
||||||
|
return core.PathIsAbs(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sprintf(format string, args ...any) string {
|
||||||
|
return core.Sprintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func split(s, sep string) []string {
|
||||||
|
return core.Split(s, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitN(s, sep string, n int) []string {
|
||||||
|
return core.SplitN(s, sep, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(s string) string {
|
||||||
|
return core.Trim(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func repeat(s string, count int) string {
|
||||||
|
if count <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := core.NewBuilder()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
buf.WriteString(s)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(format string, args ...any) {
|
||||||
|
core.Print(nil, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func println(args ...any) {
|
||||||
|
core.Println(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirSep() string {
|
||||||
|
ds := core.Env("DS")
|
||||||
|
if ds == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsRune(cutset string, target rune) bool {
|
||||||
|
for _, candidate := range cutset {
|
||||||
|
if candidate == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimCutset(s, cutset string) string {
|
||||||
|
start := 0
|
||||||
|
end := len(s)
|
||||||
|
|
||||||
|
for start < end {
|
||||||
|
r, size := utf8.DecodeRuneInString(s[start:end])
|
||||||
|
if !containsRune(cutset, r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start += size
|
||||||
|
}
|
||||||
|
|
||||||
|
for start < end {
|
||||||
|
r, size := utf8.DecodeLastRuneInString(s[start:end])
|
||||||
|
if !containsRune(cutset, r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end -= size
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func fields(s string) []string {
|
||||||
|
var out []string
|
||||||
|
start := -1
|
||||||
|
|
||||||
|
for i, r := range s {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
if start >= 0 {
|
||||||
|
out = append(out, s[start:i])
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start >= 0 {
|
||||||
|
out = append(out, s[start:])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
291
core_primitives.go
Normal file
291
core_primitives.go
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
package ansible
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
core "dappco.re/go/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stringBuffer interface {
|
||||||
|
Write([]byte) (int, error)
|
||||||
|
WriteString(string) (int, error)
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirSep() string {
|
||||||
|
ds := core.Env("DS")
|
||||||
|
if ds == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexAbsPath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return core.Env("DIR_CWD")
|
||||||
|
}
|
||||||
|
if core.PathIsAbs(path) {
|
||||||
|
return corexCleanPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd := core.Env("DIR_CWD")
|
||||||
|
if cwd == "" {
|
||||||
|
cwd = "."
|
||||||
|
}
|
||||||
|
return corexJoinPath(cwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexJoinPath(parts ...string) string {
|
||||||
|
ds := dirSep()
|
||||||
|
path := ""
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = part
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path = core.TrimSuffix(path, ds)
|
||||||
|
part = core.TrimPrefix(part, ds)
|
||||||
|
path = core.Concat(path, ds, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return core.CleanPath(path, ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexCleanPath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
return core.CleanPath(path, dirSep())
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexPathDir(path string) string {
|
||||||
|
return core.PathDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexPathBase(path string) string {
|
||||||
|
return core.PathBase(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexPathIsAbs(path string) bool {
|
||||||
|
return core.PathIsAbs(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexEnv(key string) string {
|
||||||
|
return core.Env(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexSprintf(format string, args ...any) string {
|
||||||
|
return core.Sprintf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexSprint(args ...any) string {
|
||||||
|
return core.Sprint(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexContains(s, substr string) bool {
|
||||||
|
return core.Contains(s, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexHasPrefix(s, prefix string) bool {
|
||||||
|
return core.HasPrefix(s, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexHasSuffix(s, suffix string) bool {
|
||||||
|
return core.HasSuffix(s, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexSplit(s, sep string) []string {
|
||||||
|
return core.Split(s, sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexSplitN(s, sep string, n int) []string {
|
||||||
|
return core.SplitN(s, sep, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexJoin(sep string, parts []string) string {
|
||||||
|
return core.Join(sep, parts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexLower(s string) string {
|
||||||
|
return core.Lower(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexReplaceAll(s, old, new string) string {
|
||||||
|
return core.Replace(s, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexReplaceN(s, old, new string, n int) string {
|
||||||
|
if n == 0 || old == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
return corexReplaceAll(s, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := s
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
index := corexStringIndex(result, old)
|
||||||
|
if index < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = core.Concat(result[:index], new, result[index+len(old):])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexTrimSpace(s string) string {
|
||||||
|
return core.Trim(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexTrimPrefix(s, prefix string) string {
|
||||||
|
return core.TrimPrefix(s, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexTrimCutset(s, cutset string) string {
|
||||||
|
start := 0
|
||||||
|
end := len(s)
|
||||||
|
|
||||||
|
for start < end {
|
||||||
|
r, size := utf8.DecodeRuneInString(s[start:end])
|
||||||
|
if !corexContainsRune(cutset, r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start += size
|
||||||
|
}
|
||||||
|
|
||||||
|
for start < end {
|
||||||
|
r, size := utf8.DecodeLastRuneInString(s[start:end])
|
||||||
|
if !corexContainsRune(cutset, r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end -= size
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexRepeat(s string, count int) string {
|
||||||
|
if count <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := core.NewBuilder()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
buf.WriteString(s)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexFields(s string) []string {
|
||||||
|
var out []string
|
||||||
|
start := -1
|
||||||
|
|
||||||
|
for i, r := range s {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
if start >= 0 {
|
||||||
|
out = append(out, s[start:i])
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start >= 0 {
|
||||||
|
out = append(out, s[start:])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexNewBuilder() stringBuffer {
|
||||||
|
return core.NewBuilder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexNewReader(s string) interface {
|
||||||
|
Read([]byte) (int, error)
|
||||||
|
} {
|
||||||
|
return core.NewReader(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexReadAllString(reader any) (string, error) {
|
||||||
|
result := core.ReadAll(reader)
|
||||||
|
if !result.OK {
|
||||||
|
if err, ok := result.Value.(error); ok {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", core.NewError("read content")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, ok := result.Value.(string); ok {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return corexSprint(result.Value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexWriteString(writer interface {
|
||||||
|
Write([]byte) (int, error)
|
||||||
|
}, value string) {
|
||||||
|
_, _ = writer.Write([]byte(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexContainsRune(cutset string, target rune) bool {
|
||||||
|
for _, candidate := range cutset {
|
||||||
|
if candidate == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func corexStringIndex(s, needle string) int {
|
||||||
|
if needle == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(needle) > len(s) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i+len(needle) <= len(s); i++ {
|
||||||
|
if s[i:i+len(needle)] == needle {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func absPath(path string) string { return corexAbsPath(path) }
|
||||||
|
func joinPath(parts ...string) string { return corexJoinPath(parts...) }
|
||||||
|
func cleanPath(path string) string { return corexCleanPath(path) }
|
||||||
|
func pathDir(path string) string { return corexPathDir(path) }
|
||||||
|
func pathBase(path string) string { return corexPathBase(path) }
|
||||||
|
func pathIsAbs(path string) bool { return corexPathIsAbs(path) }
|
||||||
|
func env(key string) string { return corexEnv(key) }
|
||||||
|
func sprintf(format string, args ...any) string { return corexSprintf(format, args...) }
|
||||||
|
func sprint(args ...any) string { return corexSprint(args...) }
|
||||||
|
func contains(s, substr string) bool { return corexContains(s, substr) }
|
||||||
|
func hasSuffix(s, suffix string) bool { return corexHasSuffix(s, suffix) }
|
||||||
|
func split(s, sep string) []string { return corexSplit(s, sep) }
|
||||||
|
func splitN(s, sep string, n int) []string { return corexSplitN(s, sep, n) }
|
||||||
|
func join(sep string, parts []string) string { return corexJoin(sep, parts) }
|
||||||
|
func lower(s string) string { return corexLower(s) }
|
||||||
|
func replaceAll(s, old, new string) string { return corexReplaceAll(s, old, new) }
|
||||||
|
func replaceN(s, old, new string, n int) string { return corexReplaceN(s, old, new, n) }
|
||||||
|
func trimCutset(s, cutset string) string { return corexTrimCutset(s, cutset) }
|
||||||
|
func repeat(s string, count int) string { return corexRepeat(s, count) }
|
||||||
|
func fields(s string) []string { return corexFields(s) }
|
||||||
|
func newBuilder() stringBuffer { return corexNewBuilder() }
|
||||||
|
func newReader(s string) interface{ Read([]byte) (int, error) } { return corexNewReader(s) }
|
||||||
|
func readAllString(reader any) (string, error) { return corexReadAllString(reader) }
|
||||||
|
func writeString(writer interface{ Write([]byte) (int, error) }, value string) {
|
||||||
|
corexWriteString(writer, value)
|
||||||
|
}
|
||||||
95
executor.go
95
executor.go
|
|
@ -2,11 +2,8 @@ package ansible
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -86,7 +83,7 @@ func (e *Executor) Run(ctx context.Context, playbookPath string) error {
|
||||||
|
|
||||||
for i := range plays {
|
for i := range plays {
|
||||||
if err := e.runPlay(ctx, &plays[i]); err != nil {
|
if err := e.runPlay(ctx, &plays[i]); err != nil {
|
||||||
return coreerr.E("Executor.Run", fmt.Sprintf("play %d (%s)", i, plays[i].Name), err)
|
return coreerr.E("Executor.Run", sprintf("play %d (%s)", i, plays[i].Name), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +177,7 @@ func (e *Executor) runRole(ctx context.Context, hosts []string, roleRef *RoleRef
|
||||||
// Parse role tasks
|
// Parse role tasks
|
||||||
tasks, err := e.parser.ParseRole(roleRef.Role, roleRef.TasksFrom)
|
tasks, err := e.parser.ParseRole(roleRef.Role, roleRef.TasksFrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return coreerr.E("executor.runRole", fmt.Sprintf("parse role %s", roleRef.Role), err)
|
return coreerr.E("executor.runRole", sprintf("parse role %s", roleRef.Role), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge role vars
|
// Merge role vars
|
||||||
|
|
@ -267,7 +264,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, p
|
||||||
// Get SSH client
|
// Get SSH client
|
||||||
client, err := e.getClient(host, play)
|
client, err := e.getClient(host, play)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return coreerr.E("Executor.runTaskOnHost", fmt.Sprintf("get client for %s", host), err)
|
return coreerr.E("Executor.runTaskOnHost", sprintf("get client for %s", host), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle loops
|
// Handle loops
|
||||||
|
|
@ -485,7 +482,7 @@ func (e *Executor) getHosts(pattern string) []string {
|
||||||
|
|
||||||
var filtered []string
|
var filtered []string
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
if limitSet[h] || h == e.Limit || strings.Contains(h, e.Limit) {
|
if limitSet[h] || h == e.Limit || contains(h, e.Limit) {
|
||||||
filtered = append(filtered, h)
|
filtered = append(filtered, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -582,32 +579,32 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
|
||||||
// Hostname
|
// Hostname
|
||||||
stdout, _, _, err := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
|
stdout, _, _, err := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
facts.FQDN = strings.TrimSpace(stdout)
|
facts.FQDN = corexTrimSpace(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, _, _, err = client.Run(ctx, "hostname -s 2>/dev/null || hostname")
|
stdout, _, _, err = client.Run(ctx, "hostname -s 2>/dev/null || hostname")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
facts.Hostname = strings.TrimSpace(stdout)
|
facts.Hostname = corexTrimSpace(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OS info
|
// OS info
|
||||||
stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2")
|
stdout, _, _, _ = client.Run(ctx, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2")
|
||||||
for line := range strings.SplitSeq(stdout, "\n") {
|
for _, line := range split(stdout, "\n") {
|
||||||
if strings.HasPrefix(line, "ID=") {
|
if corexHasPrefix(line, "ID=") {
|
||||||
facts.Distribution = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
|
facts.Distribution = trimCutset(corexTrimPrefix(line, "ID="), "\"")
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, "VERSION_ID=") {
|
if corexHasPrefix(line, "VERSION_ID=") {
|
||||||
facts.Version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
|
facts.Version = trimCutset(corexTrimPrefix(line, "VERSION_ID="), "\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Architecture
|
// Architecture
|
||||||
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
||||||
facts.Architecture = strings.TrimSpace(stdout)
|
facts.Architecture = corexTrimSpace(stdout)
|
||||||
|
|
||||||
// Kernel
|
// Kernel
|
||||||
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
||||||
facts.Kernel = strings.TrimSpace(stdout)
|
facts.Kernel = corexTrimSpace(stdout)
|
||||||
|
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
e.facts[host] = facts
|
e.facts[host] = facts
|
||||||
|
|
@ -650,11 +647,11 @@ func normalizeConditions(when any) []string {
|
||||||
|
|
||||||
// evalCondition evaluates a single condition.
|
// evalCondition evaluates a single condition.
|
||||||
func (e *Executor) evalCondition(cond string, host string) bool {
|
func (e *Executor) evalCondition(cond string, host string) bool {
|
||||||
cond = strings.TrimSpace(cond)
|
cond = corexTrimSpace(cond)
|
||||||
|
|
||||||
// Handle negation
|
// Handle negation
|
||||||
if strings.HasPrefix(cond, "not ") {
|
if corexHasPrefix(cond, "not ") {
|
||||||
return !e.evalCondition(strings.TrimPrefix(cond, "not "), host)
|
return !e.evalCondition(corexTrimPrefix(cond, "not "), host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle boolean literals
|
// Handle boolean literals
|
||||||
|
|
@ -667,10 +664,10 @@ func (e *Executor) evalCondition(cond string, host string) bool {
|
||||||
|
|
||||||
// Handle registered variable checks
|
// Handle registered variable checks
|
||||||
// e.g., "result is success", "result.rc == 0"
|
// e.g., "result is success", "result.rc == 0"
|
||||||
if strings.Contains(cond, " is ") {
|
if contains(cond, " is ") {
|
||||||
parts := strings.SplitN(cond, " is ", 2)
|
parts := splitN(cond, " is ", 2)
|
||||||
varName := strings.TrimSpace(parts[0])
|
varName := corexTrimSpace(parts[0])
|
||||||
check := strings.TrimSpace(parts[1])
|
check := corexTrimSpace(parts[1])
|
||||||
|
|
||||||
result := e.getRegisteredVar(host, varName)
|
result := e.getRegisteredVar(host, varName)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|
@ -694,7 +691,7 @@ func (e *Executor) evalCondition(cond string, host string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle simple var checks
|
// Handle simple var checks
|
||||||
if strings.Contains(cond, " | default(") {
|
if contains(cond, " | default(") {
|
||||||
// Extract var name and check if defined
|
// Extract var name and check if defined
|
||||||
re := regexp.MustCompile(`(\w+)\s*\|\s*default\([^)]*\)`)
|
re := regexp.MustCompile(`(\w+)\s*\|\s*default\([^)]*\)`)
|
||||||
if match := re.FindStringSubmatch(cond); len(match) > 1 {
|
if match := re.FindStringSubmatch(cond); len(match) > 1 {
|
||||||
|
|
@ -730,7 +727,7 @@ func (e *Executor) getRegisteredVar(host string, name string) *TaskResult {
|
||||||
defer e.mu.RUnlock()
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
// Handle dotted access (e.g., "result.stdout")
|
// Handle dotted access (e.g., "result.stdout")
|
||||||
parts := strings.SplitN(name, ".", 2)
|
parts := splitN(name, ".", 2)
|
||||||
varName := parts[0]
|
varName := parts[0]
|
||||||
|
|
||||||
if hostResults, ok := e.results[host]; ok {
|
if hostResults, ok := e.results[host]; ok {
|
||||||
|
|
@ -748,7 +745,7 @@ func (e *Executor) templateString(s string, host string, task *Task) string {
|
||||||
re := regexp.MustCompile(`\{\{\s*([^}]+)\s*\}\}`)
|
re := regexp.MustCompile(`\{\{\s*([^}]+)\s*\}\}`)
|
||||||
|
|
||||||
return re.ReplaceAllStringFunc(s, func(match string) string {
|
return re.ReplaceAllStringFunc(s, func(match string) string {
|
||||||
expr := strings.TrimSpace(match[2 : len(match)-2])
|
expr := corexTrimSpace(match[2 : len(match)-2])
|
||||||
return e.resolveExpr(expr, host, task)
|
return e.resolveExpr(expr, host, task)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -756,20 +753,20 @@ func (e *Executor) templateString(s string, host string, task *Task) string {
|
||||||
// resolveExpr resolves a template expression.
|
// resolveExpr resolves a template expression.
|
||||||
func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
||||||
// Handle filters
|
// Handle filters
|
||||||
if strings.Contains(expr, " | ") {
|
if contains(expr, " | ") {
|
||||||
parts := strings.SplitN(expr, " | ", 2)
|
parts := splitN(expr, " | ", 2)
|
||||||
value := e.resolveExpr(parts[0], host, task)
|
value := e.resolveExpr(parts[0], host, task)
|
||||||
return e.applyFilter(value, parts[1])
|
return e.applyFilter(value, parts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle lookups
|
// Handle lookups
|
||||||
if strings.HasPrefix(expr, "lookup(") {
|
if corexHasPrefix(expr, "lookup(") {
|
||||||
return e.handleLookup(expr)
|
return e.handleLookup(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle registered vars
|
// Handle registered vars
|
||||||
if strings.Contains(expr, ".") {
|
if contains(expr, ".") {
|
||||||
parts := strings.SplitN(expr, ".", 2)
|
parts := splitN(expr, ".", 2)
|
||||||
if result := e.getRegisteredVar(host, parts[0]); result != nil {
|
if result := e.getRegisteredVar(host, parts[0]); result != nil {
|
||||||
switch parts[1] {
|
switch parts[1] {
|
||||||
case "stdout":
|
case "stdout":
|
||||||
|
|
@ -777,24 +774,24 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
||||||
case "stderr":
|
case "stderr":
|
||||||
return result.Stderr
|
return result.Stderr
|
||||||
case "rc":
|
case "rc":
|
||||||
return fmt.Sprintf("%d", result.RC)
|
return sprintf("%d", result.RC)
|
||||||
case "changed":
|
case "changed":
|
||||||
return fmt.Sprintf("%t", result.Changed)
|
return sprintf("%t", result.Changed)
|
||||||
case "failed":
|
case "failed":
|
||||||
return fmt.Sprintf("%t", result.Failed)
|
return sprintf("%t", result.Failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check vars
|
// Check vars
|
||||||
if val, ok := e.vars[expr]; ok {
|
if val, ok := e.vars[expr]; ok {
|
||||||
return fmt.Sprintf("%v", val)
|
return sprintf("%v", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check task vars
|
// Check task vars
|
||||||
if task != nil {
|
if task != nil {
|
||||||
if val, ok := task.Vars[expr]; ok {
|
if val, ok := task.Vars[expr]; ok {
|
||||||
return fmt.Sprintf("%v", val)
|
return sprintf("%v", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -802,7 +799,7 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
||||||
if e.inventory != nil {
|
if e.inventory != nil {
|
||||||
hostVars := GetHostVars(e.inventory, host)
|
hostVars := GetHostVars(e.inventory, host)
|
||||||
if val, ok := hostVars[expr]; ok {
|
if val, ok := hostVars[expr]; ok {
|
||||||
return fmt.Sprintf("%v", val)
|
return sprintf("%v", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,15 +826,15 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
||||||
|
|
||||||
// applyFilter applies a Jinja2 filter.
|
// applyFilter applies a Jinja2 filter.
|
||||||
func (e *Executor) applyFilter(value, filter string) string {
|
func (e *Executor) applyFilter(value, filter string) string {
|
||||||
filter = strings.TrimSpace(filter)
|
filter = corexTrimSpace(filter)
|
||||||
|
|
||||||
// Handle default filter
|
// Handle default filter
|
||||||
if strings.HasPrefix(filter, "default(") {
|
if corexHasPrefix(filter, "default(") {
|
||||||
if value == "" || value == "{{ "+filter+" }}" {
|
if value == "" || value == "{{ "+filter+" }}" {
|
||||||
// Extract default value
|
// Extract default value
|
||||||
re := regexp.MustCompile(`default\(([^)]*)\)`)
|
re := regexp.MustCompile(`default\(([^)]*)\)`)
|
||||||
if match := re.FindStringSubmatch(filter); len(match) > 1 {
|
if match := re.FindStringSubmatch(filter); len(match) > 1 {
|
||||||
return strings.Trim(match[1], "'\"")
|
return trimCutset(match[1], "'\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
|
@ -845,8 +842,8 @@ func (e *Executor) applyFilter(value, filter string) string {
|
||||||
|
|
||||||
// Handle bool filter
|
// Handle bool filter
|
||||||
if filter == "bool" {
|
if filter == "bool" {
|
||||||
lower := strings.ToLower(value)
|
lowered := lower(value)
|
||||||
if lower == "true" || lower == "yes" || lower == "1" {
|
if lowered == "true" || lowered == "yes" || lowered == "1" {
|
||||||
return "true"
|
return "true"
|
||||||
}
|
}
|
||||||
return "false"
|
return "false"
|
||||||
|
|
@ -854,7 +851,7 @@ func (e *Executor) applyFilter(value, filter string) string {
|
||||||
|
|
||||||
// Handle trim
|
// Handle trim
|
||||||
if filter == "trim" {
|
if filter == "trim" {
|
||||||
return strings.TrimSpace(value)
|
return corexTrimSpace(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle b64decode
|
// Handle b64decode
|
||||||
|
|
@ -880,7 +877,7 @@ func (e *Executor) handleLookup(expr string) string {
|
||||||
|
|
||||||
switch lookupType {
|
switch lookupType {
|
||||||
case "env":
|
case "env":
|
||||||
return os.Getenv(arg)
|
return env(arg)
|
||||||
case "file":
|
case "file":
|
||||||
if data, err := coreio.Local.Read(arg); err == nil {
|
if data, err := coreio.Local.Read(arg); err == nil {
|
||||||
return data
|
return data
|
||||||
|
|
@ -978,9 +975,9 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) {
|
||||||
|
|
||||||
// Convert Jinja2 to Go template syntax (basic conversion)
|
// Convert Jinja2 to Go template syntax (basic conversion)
|
||||||
tmplContent := content
|
tmplContent := content
|
||||||
tmplContent = strings.ReplaceAll(tmplContent, "{{", "{{ .")
|
tmplContent = replaceAll(tmplContent, "{{", "{{ .")
|
||||||
tmplContent = strings.ReplaceAll(tmplContent, "{%", "{{")
|
tmplContent = replaceAll(tmplContent, "{%", "{{")
|
||||||
tmplContent = strings.ReplaceAll(tmplContent, "%}", "}}")
|
tmplContent = replaceAll(tmplContent, "%}", "}}")
|
||||||
|
|
||||||
tmpl, err := template.New("template").Parse(tmplContent)
|
tmpl, err := template.New("template").Parse(tmplContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1010,8 +1007,8 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) {
|
||||||
context["ansible_kernel"] = facts.Kernel
|
context["ansible_kernel"] = facts.Kernel
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf strings.Builder
|
buf := newBuilder()
|
||||||
if err := tmpl.Execute(&buf, context); err != nil {
|
if err := tmpl.Execute(buf, context); err != nil {
|
||||||
return e.templateString(content, host, task), nil
|
return e.templateString(content, host, task), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module dappco.re/go/core/ansible
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dappco.re/go/core v0.5.0
|
dappco.re/go/core v0.8.0-alpha.1
|
||||||
dappco.re/go/core/io v0.2.0
|
dappco.re/go/core/io v0.2.0
|
||||||
dappco.re/go/core/log v0.1.0
|
dappco.re/go/core/log v0.1.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
||||||
dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U=
|
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||||
dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||||
|
|
|
||||||
245
modules.go
245
modules.go
|
|
@ -3,11 +3,8 @@ package ansible
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
coreio "dappco.re/go/core/io"
|
coreio "dappco.re/go/core/io"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
|
|
@ -135,7 +132,7 @@ func (e *Executor) executeModule(ctx context.Context, host string, client *SSHCl
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// For unknown modules, try to execute as shell if it looks like a command
|
// For unknown modules, try to execute as shell if it looks like a command
|
||||||
if strings.Contains(task.Module, " ") || task.Module == "" {
|
if contains(task.Module, " ") || task.Module == "" {
|
||||||
return e.moduleShell(ctx, client, args)
|
return e.moduleShell(ctx, client, args)
|
||||||
}
|
}
|
||||||
return nil, coreerr.E("Executor.executeModule", "unsupported module: "+module, nil)
|
return nil, coreerr.E("Executor.executeModule", "unsupported module: "+module, nil)
|
||||||
|
|
@ -186,7 +183,7 @@ func (e *Executor) moduleShell(ctx context.Context, client *SSHClient, args map[
|
||||||
|
|
||||||
// Handle chdir
|
// Handle chdir
|
||||||
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
||||||
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
|
cmd = sprintf("cd %q && %s", chdir, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
||||||
|
|
@ -214,7 +211,7 @@ func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args ma
|
||||||
|
|
||||||
// Handle chdir
|
// Handle chdir
|
||||||
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
||||||
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
|
cmd = sprintf("cd %q && %s", chdir, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -305,20 +302,20 @@ func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Upload(ctx, strings.NewReader(content), dest, mode)
|
err = client.Upload(ctx, newReader(content), dest, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle owner/group (best-effort, errors ignored)
|
// Handle owner/group (best-effort, errors ignored)
|
||||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, dest))
|
_, _, _, _ = client.Run(ctx, sprintf("chown %s %q", owner, dest))
|
||||||
}
|
}
|
||||||
if group := getStringArg(args, "group", ""); group != "" {
|
if group := getStringArg(args, "group", ""); group != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, dest))
|
_, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, dest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil
|
return &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||||
|
|
@ -341,12 +338,12 @@ func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = client.Upload(ctx, strings.NewReader(content), dest, mode)
|
err = client.Upload(ctx, newReader(content), dest, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil
|
return &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
||||||
|
|
@ -363,21 +360,21 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s
|
||||||
switch state {
|
switch state {
|
||||||
case "directory":
|
case "directory":
|
||||||
mode := getStringArg(args, "mode", "0755")
|
mode := getStringArg(args, "mode", "0755")
|
||||||
cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
|
cmd := sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case "absent":
|
case "absent":
|
||||||
cmd := fmt.Sprintf("rm -rf %q", path)
|
cmd := sprintf("rm -rf %q", path)
|
||||||
_, stderr, rc, err := client.Run(ctx, cmd)
|
_, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case "touch":
|
case "touch":
|
||||||
cmd := fmt.Sprintf("touch %q", path)
|
cmd := sprintf("touch %q", path)
|
||||||
_, stderr, rc, err := client.Run(ctx, cmd)
|
_, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||||
|
|
@ -388,7 +385,7 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s
|
||||||
if src == "" {
|
if src == "" {
|
||||||
return nil, coreerr.E("Executor.moduleFile", "src required for link state", nil)
|
return nil, coreerr.E("Executor.moduleFile", "src required for link state", nil)
|
||||||
}
|
}
|
||||||
cmd := fmt.Sprintf("ln -sf %q %q", src, path)
|
cmd := sprintf("ln -sf %q %q", src, path)
|
||||||
_, stderr, rc, err := client.Run(ctx, cmd)
|
_, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||||
|
|
@ -397,20 +394,20 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s
|
||||||
case "file":
|
case "file":
|
||||||
// Ensure file exists and set permissions
|
// Ensure file exists and set permissions
|
||||||
if mode := getStringArg(args, "mode", ""); mode != "" {
|
if mode := getStringArg(args, "mode", ""); mode != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, path))
|
_, _, _, _ = client.Run(ctx, sprintf("chmod %s %q", mode, path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle owner/group (best-effort, errors ignored)
|
// Handle owner/group (best-effort, errors ignored)
|
||||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown %s %q", owner, path))
|
_, _, _, _ = client.Run(ctx, sprintf("chown %s %q", owner, path))
|
||||||
}
|
}
|
||||||
if group := getStringArg(args, "group", ""); group != "" {
|
if group := getStringArg(args, "group", ""); group != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chgrp %s %q", group, path))
|
_, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, path))
|
||||||
}
|
}
|
||||||
if recurse := getBoolArg(args, "recurse", false); recurse {
|
if recurse := getBoolArg(args, "recurse", false); recurse {
|
||||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chown -R %s %q", owner, path))
|
_, _, _, _ = client.Run(ctx, sprintf("chown -R %s %q", owner, path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +429,7 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
if regexp != "" {
|
if regexp != "" {
|
||||||
cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexp, path)
|
cmd := sprintf("sed -i '/%s/d' %q", regexp, path)
|
||||||
_, stderr, rc, _ := client.Run(ctx, cmd)
|
_, stderr, rc, _ := client.Run(ctx, cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||||
|
|
@ -442,17 +439,17 @@ func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args
|
||||||
// state == present
|
// state == present
|
||||||
if regexp != "" {
|
if regexp != "" {
|
||||||
// Replace line matching regexp
|
// Replace line matching regexp
|
||||||
escapedLine := strings.ReplaceAll(line, "/", "\\/")
|
escapedLine := replaceAll(line, "/", "\\/")
|
||||||
cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path)
|
cmd := sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path)
|
||||||
_, _, rc, _ := client.Run(ctx, cmd)
|
_, _, rc, _ := client.Run(ctx, cmd)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
// Line not found, append
|
// Line not found, append
|
||||||
cmd = fmt.Sprintf("echo %q >> %q", line, path)
|
cmd = sprintf("echo %q >> %q", line, path)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
} else if line != "" {
|
} else if line != "" {
|
||||||
// Ensure line is present
|
// Ensure line is present
|
||||||
cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
|
cmd := sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -512,7 +509,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create dest directory
|
// Create dest directory
|
||||||
if err := coreio.Local.EnsureDir(filepath.Dir(dest)); err != nil {
|
if err := coreio.Local.EnsureDir(pathDir(dest)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,7 +517,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{Changed: true, Msg: fmt.Sprintf("fetched %s to %s", src, dest)}, nil
|
return &TaskResult{Changed: true, Msg: sprintf("fetched %s to %s", src, dest)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
||||||
|
|
@ -531,7 +528,7 @@ func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use curl or wget
|
// Use curl or wget
|
||||||
cmd := fmt.Sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url)
|
cmd := sprintf("curl -fsSL -o %q %q || wget -q -O %q %q", dest, url, dest, url)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||||
|
|
@ -539,7 +536,7 @@ func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map
|
||||||
|
|
||||||
// Set mode if specified (best-effort)
|
// Set mode if specified (best-effort)
|
||||||
if mode := getStringArg(args, "mode", ""); mode != "" {
|
if mode := getStringArg(args, "mode", ""); mode != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod %s %q", mode, dest))
|
_, _, _, _ = client.Run(ctx, sprintf("chmod %s %q", mode, dest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
|
|
@ -561,12 +558,12 @@ func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[st
|
||||||
switch state {
|
switch state {
|
||||||
case "present", "installed":
|
case "present", "installed":
|
||||||
if name != "" {
|
if name != "" {
|
||||||
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
|
||||||
}
|
}
|
||||||
case "absent", "removed":
|
case "absent", "removed":
|
||||||
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
|
||||||
case "latest":
|
case "latest":
|
||||||
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd == "" {
|
if cmd == "" {
|
||||||
|
|
@ -588,7 +585,7 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
if keyring != "" {
|
if keyring != "" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", keyring))
|
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", keyring))
|
||||||
}
|
}
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -599,9 +596,9 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map
|
||||||
|
|
||||||
var cmd string
|
var cmd string
|
||||||
if keyring != "" {
|
if keyring != "" {
|
||||||
cmd = fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
|
cmd = sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
|
||||||
} else {
|
} else {
|
||||||
cmd = fmt.Sprintf("curl -fsSL %q | apt-key add -", url)
|
cmd = sprintf("curl -fsSL %q | apt-key add -", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -623,19 +620,19 @@ func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, a
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
// Generate filename from repo
|
// Generate filename from repo
|
||||||
filename = strings.ReplaceAll(repo, " ", "-")
|
filename = replaceAll(repo, " ", "-")
|
||||||
filename = strings.ReplaceAll(filename, "/", "-")
|
filename = replaceAll(filename, "/", "-")
|
||||||
filename = strings.ReplaceAll(filename, ":", "")
|
filename = replaceAll(filename, ":", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
path := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", filename)
|
path := sprintf("/etc/apt/sources.list.d/%s.list", filename)
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", path))
|
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", path))
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := fmt.Sprintf("echo %q > %q", repo, path)
|
cmd := sprintf("echo %q > %q", repo, path)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||||
|
|
@ -652,9 +649,9 @@ func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, a
|
||||||
func (e *Executor) modulePackage(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
func (e *Executor) modulePackage(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
||||||
// Detect package manager and delegate
|
// Detect package manager and delegate
|
||||||
stdout, _, _, _ := client.Run(ctx, "which apt-get yum dnf 2>/dev/null | head -1")
|
stdout, _, _, _ := client.Run(ctx, "which apt-get yum dnf 2>/dev/null | head -1")
|
||||||
stdout = strings.TrimSpace(stdout)
|
stdout = corexTrimSpace(stdout)
|
||||||
|
|
||||||
if strings.Contains(stdout, "apt") {
|
if contains(stdout, "apt") {
|
||||||
return e.moduleApt(ctx, client, args)
|
return e.moduleApt(ctx, client, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,11 +667,11 @@ func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[st
|
||||||
var cmd string
|
var cmd string
|
||||||
switch state {
|
switch state {
|
||||||
case "present", "installed":
|
case "present", "installed":
|
||||||
cmd = fmt.Sprintf("%s install %s", executable, name)
|
cmd = sprintf("%s install %s", executable, name)
|
||||||
case "absent", "removed":
|
case "absent", "removed":
|
||||||
cmd = fmt.Sprintf("%s uninstall -y %s", executable, name)
|
cmd = sprintf("%s uninstall -y %s", executable, name)
|
||||||
case "latest":
|
case "latest":
|
||||||
cmd = fmt.Sprintf("%s install --upgrade %s", executable, name)
|
cmd = sprintf("%s install --upgrade %s", executable, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -701,21 +698,21 @@ func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args ma
|
||||||
if state != "" {
|
if state != "" {
|
||||||
switch state {
|
switch state {
|
||||||
case "started":
|
case "started":
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl start %s", name))
|
cmds = append(cmds, sprintf("systemctl start %s", name))
|
||||||
case "stopped":
|
case "stopped":
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name))
|
cmds = append(cmds, sprintf("systemctl stop %s", name))
|
||||||
case "restarted":
|
case "restarted":
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name))
|
cmds = append(cmds, sprintf("systemctl restart %s", name))
|
||||||
case "reloaded":
|
case "reloaded":
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name))
|
cmds = append(cmds, sprintf("systemctl reload %s", name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabled != nil {
|
if enabled != nil {
|
||||||
if getBoolArg(args, "enabled", false) {
|
if getBoolArg(args, "enabled", false) {
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name))
|
cmds = append(cmds, sprintf("systemctl enable %s", name))
|
||||||
} else {
|
} else {
|
||||||
cmds = append(cmds, fmt.Sprintf("systemctl disable %s", name))
|
cmds = append(cmds, sprintf("systemctl disable %s", name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -749,7 +746,7 @@ func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[s
|
||||||
}
|
}
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
cmd := fmt.Sprintf("userdel -r %s 2>/dev/null || true", name)
|
cmd := sprintf("userdel -r %s 2>/dev/null || true", name)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -780,12 +777,12 @@ func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try usermod first, then useradd
|
// Try usermod first, then useradd
|
||||||
optsStr := strings.Join(opts, " ")
|
optsStr := join(" ", opts)
|
||||||
var cmd string
|
var cmd string
|
||||||
if optsStr == "" {
|
if optsStr == "" {
|
||||||
cmd = fmt.Sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
|
cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
|
||||||
} else {
|
} else {
|
||||||
cmd = fmt.Sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
|
cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
|
||||||
name, optsStr, name, optsStr, name)
|
name, optsStr, name, optsStr, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -806,7 +803,7 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[
|
||||||
}
|
}
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
cmd := fmt.Sprintf("groupdel %s 2>/dev/null || true", name)
|
cmd := sprintf("groupdel %s 2>/dev/null || true", name)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -819,8 +816,8 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[
|
||||||
opts = append(opts, "-r")
|
opts = append(opts, "-r")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
|
cmd := sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
|
||||||
name, strings.Join(opts, " "), name)
|
name, join(" ", opts), name)
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
|
|
@ -847,7 +844,7 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st
|
||||||
// Headers
|
// Headers
|
||||||
if headers, ok := args["headers"].(map[string]any); ok {
|
if headers, ok := args["headers"].(map[string]any); ok {
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
curlOpts = append(curlOpts, "-H", fmt.Sprintf("%s: %v", k, v))
|
curlOpts = append(curlOpts, "-H", sprintf("%s: %v", k, v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,14 +856,14 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st
|
||||||
// Status code
|
// Status code
|
||||||
curlOpts = append(curlOpts, "-w", "\\n%{http_code}")
|
curlOpts = append(curlOpts, "-w", "\\n%{http_code}")
|
||||||
|
|
||||||
cmd := fmt.Sprintf("curl %s %q", strings.Join(curlOpts, " "), url)
|
cmd := sprintf("curl %s %q", join(" ", curlOpts), url)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse status code from last line
|
// Parse status code from last line
|
||||||
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
lines := split(corexTrimSpace(stdout), "\n")
|
||||||
statusCode := 0
|
statusCode := 0
|
||||||
if len(lines) > 0 {
|
if len(lines) > 0 {
|
||||||
statusCode, _ = strconv.Atoi(lines[len(lines)-1])
|
statusCode, _ = strconv.Atoi(lines[len(lines)-1])
|
||||||
|
|
@ -895,7 +892,7 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st
|
||||||
func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) {
|
func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) {
|
||||||
msg := getStringArg(args, "msg", "")
|
msg := getStringArg(args, "msg", "")
|
||||||
if v, ok := args["var"]; ok {
|
if v, ok := args["var"]; ok {
|
||||||
msg = fmt.Sprintf("%v = %v", v, e.vars[fmt.Sprintf("%v", v)])
|
msg = sprintf("%v = %v", v, e.vars[sprintf("%v", v)])
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{
|
return &TaskResult{
|
||||||
|
|
@ -921,7 +918,7 @@ func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult,
|
||||||
conditions := normalizeConditions(that)
|
conditions := normalizeConditions(that)
|
||||||
for _, cond := range conditions {
|
for _, cond := range conditions {
|
||||||
if !e.evalCondition(cond, host) {
|
if !e.evalCondition(cond, host) {
|
||||||
msg := getStringArg(args, "fail_msg", fmt.Sprintf("Assertion failed: %s", cond))
|
msg := getStringArg(args, "fail_msg", sprintf("Assertion failed: %s", cond))
|
||||||
return &TaskResult{Failed: true, Msg: msg}, nil
|
return &TaskResult{Failed: true, Msg: msg}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -999,7 +996,7 @@ func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args ma
|
||||||
}
|
}
|
||||||
|
|
||||||
if port > 0 && state == "started" {
|
if port > 0 && state == "started" {
|
||||||
cmd := fmt.Sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'",
|
cmd := sprintf("timeout %d bash -c 'until nc -z %s %d; do sleep 1; done'",
|
||||||
timeout, host, port)
|
timeout, host, port)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
|
|
@ -1025,9 +1022,9 @@ func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[st
|
||||||
var cmd string
|
var cmd string
|
||||||
if exists {
|
if exists {
|
||||||
// Fetch and checkout (force to ensure clean state)
|
// Fetch and checkout (force to ensure clean state)
|
||||||
cmd = fmt.Sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
|
cmd = sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
|
||||||
} else {
|
} else {
|
||||||
cmd = fmt.Sprintf("git clone %q %q && cd %q && git checkout %q",
|
cmd = sprintf("git clone %q %q && cd %q && git checkout %q",
|
||||||
repo, dest, dest, version)
|
repo, dest, dest, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1049,7 +1046,7 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create dest directory (best-effort)
|
// Create dest directory (best-effort)
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q", dest))
|
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q", dest))
|
||||||
|
|
||||||
var cmd string
|
var cmd string
|
||||||
if !remote {
|
if !remote {
|
||||||
|
|
@ -1058,28 +1055,28 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, coreerr.E("Executor.moduleUnarchive", "read src", err)
|
return nil, coreerr.E("Executor.moduleUnarchive", "read src", err)
|
||||||
}
|
}
|
||||||
tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src)
|
tmpPath := "/tmp/ansible_unarchive_" + pathBase(src)
|
||||||
err = client.Upload(ctx, strings.NewReader(data), tmpPath, 0644)
|
err = client.Upload(ctx, newReader(data), tmpPath, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
src = tmpPath
|
src = tmpPath
|
||||||
defer func() { _, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", tmpPath)) }()
|
defer func() { _, _, _, _ = client.Run(ctx, sprintf("rm -f %q", tmpPath)) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect archive type and extract
|
// Detect archive type and extract
|
||||||
if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") {
|
if hasSuffix(src, ".tar.gz") || hasSuffix(src, ".tgz") {
|
||||||
cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest)
|
cmd = sprintf("tar -xzf %q -C %q", src, dest)
|
||||||
} else if strings.HasSuffix(src, ".tar.xz") {
|
} else if hasSuffix(src, ".tar.xz") {
|
||||||
cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest)
|
cmd = sprintf("tar -xJf %q -C %q", src, dest)
|
||||||
} else if strings.HasSuffix(src, ".tar.bz2") {
|
} else if hasSuffix(src, ".tar.bz2") {
|
||||||
cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest)
|
cmd = sprintf("tar -xjf %q -C %q", src, dest)
|
||||||
} else if strings.HasSuffix(src, ".tar") {
|
} else if hasSuffix(src, ".tar") {
|
||||||
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest)
|
cmd = sprintf("tar -xf %q -C %q", src, dest)
|
||||||
} else if strings.HasSuffix(src, ".zip") {
|
} else if hasSuffix(src, ".zip") {
|
||||||
cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest)
|
cmd = sprintf("unzip -o %q -d %q", src, dest)
|
||||||
} else {
|
} else {
|
||||||
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) // Guess tar
|
cmd = sprintf("tar -xf %q -C %q", src, dest) // Guess tar
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -1097,7 +1094,7 @@ func getStringArg(args map[string]any, key, def string) string {
|
||||||
if s, ok := v.(string); ok {
|
if s, ok := v.(string); ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%v", v)
|
return sprintf("%v", v)
|
||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|
@ -1108,8 +1105,8 @@ func getBoolArg(args map[string]any, key string, def bool) bool {
|
||||||
case bool:
|
case bool:
|
||||||
return b
|
return b
|
||||||
case string:
|
case string:
|
||||||
lower := strings.ToLower(b)
|
lowered := lower(b)
|
||||||
return lower == "true" || lower == "yes" || lower == "1"
|
return lowered == "true" || lowered == "yes" || lowered == "1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
|
|
@ -1124,14 +1121,14 @@ func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args m
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set hostname
|
// Set hostname
|
||||||
cmd := fmt.Sprintf("hostnamectl set-hostname %q || hostname %q", name, name)
|
cmd := sprintf("hostnamectl set-hostname %q || hostname %q", name, name)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update /etc/hosts if needed (best-effort)
|
// Update /etc/hosts if needed (best-effort)
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name))
|
_, _, _, _ = client.Run(ctx, sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name))
|
||||||
|
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -1147,13 +1144,13 @@ func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
// Remove from sysctl.conf
|
// Remove from sysctl.conf
|
||||||
cmd := fmt.Sprintf("sed -i '/%s/d' /etc/sysctl.conf", name)
|
cmd := sprintf("sed -i '/%s/d' /etc/sysctl.conf", name)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set value
|
// Set value
|
||||||
cmd := fmt.Sprintf("sysctl -w %s=%s", name, value)
|
cmd := sprintf("sysctl -w %s=%s", name, value)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||||
|
|
@ -1161,7 +1158,7 @@ func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map
|
||||||
|
|
||||||
// Persist if requested (best-effort)
|
// Persist if requested (best-effort)
|
||||||
if getBoolArg(args, "sysctl_set", true) {
|
if getBoolArg(args, "sysctl_set", true) {
|
||||||
cmd = fmt.Sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf",
|
cmd = sprintf("grep -q '^%s' /etc/sysctl.conf && sed -i 's/^%s.*/%s=%s/' /etc/sysctl.conf || echo '%s=%s' >> /etc/sysctl.conf",
|
||||||
name, name, name, value, name, value)
|
name, name, name, value, name, value)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
@ -1184,7 +1181,7 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
if name != "" {
|
if name != "" {
|
||||||
// Remove by name (comment marker)
|
// Remove by name (comment marker)
|
||||||
cmd := fmt.Sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -",
|
cmd := sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -",
|
||||||
user, name, job, user)
|
user, name, job, user)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
@ -1192,11 +1189,11 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build cron entry
|
// Build cron entry
|
||||||
schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
|
schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
|
||||||
entry := fmt.Sprintf("%s %s # %s", schedule, job, name)
|
entry := sprintf("%s %s # %s", schedule, job, name)
|
||||||
|
|
||||||
// Add to crontab
|
// Add to crontab
|
||||||
cmd := fmt.Sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -",
|
cmd := sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -",
|
||||||
user, name, entry, user)
|
user, name, entry, user)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
|
|
@ -1220,14 +1217,14 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, arg
|
||||||
state := getStringArg(args, "state", "present")
|
state := getStringArg(args, "state", "present")
|
||||||
create := getBoolArg(args, "create", false)
|
create := getBoolArg(args, "create", false)
|
||||||
|
|
||||||
beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1)
|
beginMarker := replaceN(marker, "{mark}", "BEGIN", 1)
|
||||||
endMarker := strings.Replace(marker, "{mark}", "END", 1)
|
endMarker := replaceN(marker, "{mark}", "END", 1)
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
// Remove block
|
// Remove block
|
||||||
cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q",
|
cmd := sprintf("sed -i '/%s/,/%s/d' %q",
|
||||||
strings.ReplaceAll(beginMarker, "/", "\\/"),
|
replaceAll(beginMarker, "/", "\\/"),
|
||||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
replaceAll(endMarker, "/", "\\/"),
|
||||||
path)
|
path)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
|
|
@ -1235,20 +1232,20 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, arg
|
||||||
|
|
||||||
// Create file if needed (best-effort)
|
// Create file if needed (best-effort)
|
||||||
if create {
|
if create {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("touch %q", path))
|
_, _, _, _ = client.Run(ctx, sprintf("touch %q", path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing block and add new one
|
// Remove existing block and add new one
|
||||||
escapedBlock := strings.ReplaceAll(block, "'", "'\\''")
|
escapedBlock := replaceAll(block, "'", "'\\''")
|
||||||
cmd := fmt.Sprintf(`
|
cmd := sprintf(`
|
||||||
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
|
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
|
||||||
cat >> %q << 'BLOCK_EOF'
|
cat >> %q << 'BLOCK_EOF'
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
BLOCK_EOF
|
BLOCK_EOF
|
||||||
`, strings.ReplaceAll(beginMarker, "/", "\\/"),
|
`, replaceAll(beginMarker, "/", "\\/"),
|
||||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
replaceAll(endMarker, "/", "\\/"),
|
||||||
path, path, beginMarker, escapedBlock, endMarker)
|
path, path, beginMarker, escapedBlock, endMarker)
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
||||||
|
|
@ -1294,10 +1291,10 @@ func (e *Executor) moduleReboot(ctx context.Context, client *SSHClient, args map
|
||||||
msg := getStringArg(args, "msg", "Reboot initiated by Ansible")
|
msg := getStringArg(args, "msg", "Reboot initiated by Ansible")
|
||||||
|
|
||||||
if preRebootDelay > 0 {
|
if preRebootDelay > 0 {
|
||||||
cmd := fmt.Sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg)
|
cmd := sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
} else {
|
} else {
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("shutdown -r now '%s' &", msg))
|
_, _, _, _ = client.Run(ctx, sprintf("shutdown -r now '%s' &", msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil
|
return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil
|
||||||
|
|
@ -1336,13 +1333,13 @@ func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[st
|
||||||
if rule != "" && port != "" {
|
if rule != "" && port != "" {
|
||||||
switch rule {
|
switch rule {
|
||||||
case "allow":
|
case "allow":
|
||||||
cmd = fmt.Sprintf("ufw allow %s/%s", port, proto)
|
cmd = sprintf("ufw allow %s/%s", port, proto)
|
||||||
case "deny":
|
case "deny":
|
||||||
cmd = fmt.Sprintf("ufw deny %s/%s", port, proto)
|
cmd = sprintf("ufw deny %s/%s", port, proto)
|
||||||
case "reject":
|
case "reject":
|
||||||
cmd = fmt.Sprintf("ufw reject %s/%s", port, proto)
|
cmd = sprintf("ufw reject %s/%s", port, proto)
|
||||||
case "limit":
|
case "limit":
|
||||||
cmd = fmt.Sprintf("ufw limit %s/%s", port, proto)
|
cmd = sprintf("ufw limit %s/%s", port, proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -1364,11 +1361,11 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's home directory
|
// Get user's home directory
|
||||||
stdout, _, _, err := client.Run(ctx, fmt.Sprintf("getent passwd %s | cut -d: -f6", user))
|
stdout, _, _, err := client.Run(ctx, sprintf("getent passwd %s | cut -d: -f6", user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, coreerr.E("Executor.moduleAuthorizedKey", "get home dir", err)
|
return nil, coreerr.E("Executor.moduleAuthorizedKey", "get home dir", err)
|
||||||
}
|
}
|
||||||
home := strings.TrimSpace(stdout)
|
home := corexTrimSpace(stdout)
|
||||||
if home == "" {
|
if home == "" {
|
||||||
home = "/root"
|
home = "/root"
|
||||||
if user != "root" {
|
if user != "root" {
|
||||||
|
|
@ -1376,22 +1373,22 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authKeysPath := filepath.Join(home, ".ssh", "authorized_keys")
|
authKeysPath := joinPath(home, ".ssh", "authorized_keys")
|
||||||
|
|
||||||
if state == "absent" {
|
if state == "absent" {
|
||||||
// Remove key
|
// Remove key
|
||||||
escapedKey := strings.ReplaceAll(key, "/", "\\/")
|
escapedKey := replaceAll(key, "/", "\\/")
|
||||||
cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
|
cmd := sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
|
||||||
_, _, _, _ = client.Run(ctx, cmd)
|
_, _, _, _ = client.Run(ctx, cmd)
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure .ssh directory exists (best-effort)
|
// Ensure .ssh directory exists (best-effort)
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
|
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
|
||||||
filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath)))
|
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
|
||||||
|
|
||||||
// Add key if not present
|
// Add key if not present
|
||||||
cmd := fmt.Sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q",
|
cmd := sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q",
|
||||||
key[:40], authKeysPath, key, authKeysPath)
|
key[:40], authKeysPath, key, authKeysPath)
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
if err != nil || rc != 0 {
|
if err != nil || rc != 0 {
|
||||||
|
|
@ -1399,7 +1396,7 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix permissions (best-effort)
|
// Fix permissions (best-effort)
|
||||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("chmod 600 %q && chown %s:%s %q",
|
_, _, _, _ = client.Run(ctx, sprintf("chmod 600 %q && chown %s:%s %q",
|
||||||
authKeysPath, user, user, authKeysPath))
|
authKeysPath, user, user, authKeysPath))
|
||||||
|
|
||||||
return &TaskResult{Changed: true}, nil
|
return &TaskResult{Changed: true}, nil
|
||||||
|
|
@ -1416,13 +1413,13 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a
|
||||||
var cmd string
|
var cmd string
|
||||||
switch state {
|
switch state {
|
||||||
case "present":
|
case "present":
|
||||||
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
|
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
|
||||||
case "absent":
|
case "absent":
|
||||||
cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc)
|
cmd = sprintf("cd %q && docker compose down", projectSrc)
|
||||||
case "restarted":
|
case "restarted":
|
||||||
cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc)
|
cmd = sprintf("cd %q && docker compose restart", projectSrc)
|
||||||
default:
|
default:
|
||||||
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
|
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||||
|
|
@ -1431,7 +1428,7 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic for changed
|
// Heuristic for changed
|
||||||
changed := !strings.Contains(stdout, "Up to date") && !strings.Contains(stderr, "Up to date")
|
changed := !contains(stdout, "Up to date") && !contains(stderr, "Up to date")
|
||||||
|
|
||||||
return &TaskResult{Changed: changed, Stdout: stdout}, nil
|
return &TaskResult{Changed: changed, Stdout: stdout}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
parser.go
41
parser.go
|
|
@ -1,12 +1,9 @@
|
||||||
package ansible
|
package ansible
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
coreio "dappco.re/go/core/io"
|
coreio "dappco.re/go/core/io"
|
||||||
coreerr "dappco.re/go/core/log"
|
coreerr "dappco.re/go/core/log"
|
||||||
|
|
@ -42,7 +39,7 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) {
|
||||||
// Process each play
|
// Process each play
|
||||||
for i := range plays {
|
for i := range plays {
|
||||||
if err := p.processPlay(&plays[i]); err != nil {
|
if err := p.processPlay(&plays[i]); err != nil {
|
||||||
return nil, coreerr.E("Parser.ParsePlaybook", fmt.Sprintf("process play %d", i), err)
|
return nil, coreerr.E("Parser.ParsePlaybook", sprintf("process play %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +90,7 @@ func (p *Parser) ParseTasks(path string) ([]Task, error) {
|
||||||
|
|
||||||
for i := range tasks {
|
for i := range tasks {
|
||||||
if err := p.extractModule(&tasks[i]); err != nil {
|
if err := p.extractModule(&tasks[i]); err != nil {
|
||||||
return nil, coreerr.E("Parser.ParseTasks", fmt.Sprintf("task %d", i), err)
|
return nil, coreerr.E("Parser.ParseTasks", sprintf("task %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,21 +121,21 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
|
||||||
// Search paths for roles (in order of precedence)
|
// Search paths for roles (in order of precedence)
|
||||||
searchPaths := []string{
|
searchPaths := []string{
|
||||||
// Relative to playbook
|
// Relative to playbook
|
||||||
filepath.Join(p.basePath, "roles", name, "tasks", tasksFrom),
|
joinPath(p.basePath, "roles", name, "tasks", tasksFrom),
|
||||||
// Parent directory roles
|
// Parent directory roles
|
||||||
filepath.Join(filepath.Dir(p.basePath), "roles", name, "tasks", tasksFrom),
|
joinPath(pathDir(p.basePath), "roles", name, "tasks", tasksFrom),
|
||||||
// Sibling roles directory
|
// Sibling roles directory
|
||||||
filepath.Join(p.basePath, "..", "roles", name, "tasks", tasksFrom),
|
joinPath(p.basePath, "..", "roles", name, "tasks", tasksFrom),
|
||||||
// playbooks/roles pattern
|
// playbooks/roles pattern
|
||||||
filepath.Join(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom),
|
joinPath(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom),
|
||||||
// Common DevOps structure
|
// Common DevOps structure
|
||||||
filepath.Join(filepath.Dir(filepath.Dir(p.basePath)), "roles", name, "tasks", tasksFrom),
|
joinPath(pathDir(pathDir(p.basePath)), "roles", name, "tasks", tasksFrom),
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasksPath string
|
var tasksPath string
|
||||||
for _, sp := range searchPaths {
|
for _, sp := range searchPaths {
|
||||||
// Clean the path to resolve .. segments
|
// Clean the path to resolve .. segments
|
||||||
sp = filepath.Clean(sp)
|
sp = cleanPath(sp)
|
||||||
if coreio.Local.Exists(sp) {
|
if coreio.Local.Exists(sp) {
|
||||||
tasksPath = sp
|
tasksPath = sp
|
||||||
break
|
break
|
||||||
|
|
@ -146,11 +143,11 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tasksPath == "" {
|
if tasksPath == "" {
|
||||||
return nil, coreerr.E("Parser.ParseRole", fmt.Sprintf("role %s not found in search paths: %v", name, searchPaths), nil)
|
return nil, coreerr.E("Parser.ParseRole", sprintf("role %s not found in search paths: %v", name, searchPaths), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load role defaults
|
// Load role defaults
|
||||||
defaultsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "defaults", "main.yml")
|
defaultsPath := joinPath(pathDir(pathDir(tasksPath)), "defaults", "main.yml")
|
||||||
if data, err := coreio.Local.Read(defaultsPath); err == nil {
|
if data, err := coreio.Local.Read(defaultsPath); err == nil {
|
||||||
var defaults map[string]any
|
var defaults map[string]any
|
||||||
if yaml.Unmarshal([]byte(data), &defaults) == nil {
|
if yaml.Unmarshal([]byte(data), &defaults) == nil {
|
||||||
|
|
@ -163,7 +160,7 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load role vars
|
// Load role vars
|
||||||
varsPath := filepath.Join(filepath.Dir(filepath.Dir(tasksPath)), "vars", "main.yml")
|
varsPath := joinPath(pathDir(pathDir(tasksPath)), "vars", "main.yml")
|
||||||
if data, err := coreio.Local.Read(varsPath); err == nil {
|
if data, err := coreio.Local.Read(varsPath); err == nil {
|
||||||
var roleVars map[string]any
|
var roleVars map[string]any
|
||||||
if yaml.Unmarshal([]byte(data), &roleVars) == nil {
|
if yaml.Unmarshal([]byte(data), &roleVars) == nil {
|
||||||
|
|
@ -185,25 +182,25 @@ func (p *Parser) processPlay(play *Play) error {
|
||||||
|
|
||||||
for i := range play.PreTasks {
|
for i := range play.PreTasks {
|
||||||
if err := p.extractModule(&play.PreTasks[i]); err != nil {
|
if err := p.extractModule(&play.PreTasks[i]); err != nil {
|
||||||
return coreerr.E("Parser.processPlay", fmt.Sprintf("pre_task %d", i), err)
|
return coreerr.E("Parser.processPlay", sprintf("pre_task %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range play.Tasks {
|
for i := range play.Tasks {
|
||||||
if err := p.extractModule(&play.Tasks[i]); err != nil {
|
if err := p.extractModule(&play.Tasks[i]); err != nil {
|
||||||
return coreerr.E("Parser.processPlay", fmt.Sprintf("task %d", i), err)
|
return coreerr.E("Parser.processPlay", sprintf("task %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range play.PostTasks {
|
for i := range play.PostTasks {
|
||||||
if err := p.extractModule(&play.PostTasks[i]); err != nil {
|
if err := p.extractModule(&play.PostTasks[i]); err != nil {
|
||||||
return coreerr.E("Parser.processPlay", fmt.Sprintf("post_task %d", i), err)
|
return coreerr.E("Parser.processPlay", sprintf("post_task %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range play.Handlers {
|
for i := range play.Handlers {
|
||||||
if err := p.extractModule(&play.Handlers[i]); err != nil {
|
if err := p.extractModule(&play.Handlers[i]); err != nil {
|
||||||
return coreerr.E("Parser.processPlay", fmt.Sprintf("handler %d", i), err)
|
return coreerr.E("Parser.processPlay", sprintf("handler %d", i), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,20 +305,20 @@ func isModule(key string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Also check without ansible.builtin. prefix
|
// Also check without ansible.builtin. prefix
|
||||||
if strings.HasPrefix(m, "ansible.builtin.") {
|
if corexHasPrefix(m, "ansible.builtin.") {
|
||||||
if key == strings.TrimPrefix(m, "ansible.builtin.") {
|
if key == corexTrimPrefix(m, "ansible.builtin.") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Accept any key with dots (likely a module)
|
// Accept any key with dots (likely a module)
|
||||||
return strings.Contains(key, ".")
|
return contains(key, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeModule normalizes a module name to its canonical form.
|
// NormalizeModule normalizes a module name to its canonical form.
|
||||||
func NormalizeModule(name string) string {
|
func NormalizeModule(name string) string {
|
||||||
// Add ansible.builtin. prefix if missing
|
// Add ansible.builtin. prefix if missing
|
||||||
if !strings.Contains(name, ".") {
|
if !contains(name, ".") {
|
||||||
return "ansible.builtin." + name
|
return "ansible.builtin." + name
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|
|
||||||
80
ssh.go
80
ssh.go
|
|
@ -3,12 +3,9 @@ package ansible
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -87,9 +84,8 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
||||||
// Try key-based auth first
|
// Try key-based auth first
|
||||||
if c.keyFile != "" {
|
if c.keyFile != "" {
|
||||||
keyPath := c.keyFile
|
keyPath := c.keyFile
|
||||||
if strings.HasPrefix(keyPath, "~") {
|
if corexHasPrefix(keyPath, "~") {
|
||||||
home, _ := os.UserHomeDir()
|
keyPath = joinPath(env("DIR_HOME"), keyPath[1:])
|
||||||
keyPath = filepath.Join(home, keyPath[1:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if key, err := coreio.Local.Read(keyPath); err == nil {
|
if key, err := coreio.Local.Read(keyPath); err == nil {
|
||||||
|
|
@ -101,10 +97,10 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
||||||
|
|
||||||
// Try default SSH keys
|
// Try default SSH keys
|
||||||
if len(authMethods) == 0 {
|
if len(authMethods) == 0 {
|
||||||
home, _ := os.UserHomeDir()
|
home := env("DIR_HOME")
|
||||||
defaultKeys := []string{
|
defaultKeys := []string{
|
||||||
filepath.Join(home, ".ssh", "id_ed25519"),
|
joinPath(home, ".ssh", "id_ed25519"),
|
||||||
filepath.Join(home, ".ssh", "id_rsa"),
|
joinPath(home, ".ssh", "id_rsa"),
|
||||||
}
|
}
|
||||||
for _, keyPath := range defaultKeys {
|
for _, keyPath := range defaultKeys {
|
||||||
if key, err := coreio.Local.Read(keyPath); err == nil {
|
if key, err := coreio.Local.Read(keyPath); err == nil {
|
||||||
|
|
@ -135,15 +131,15 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
||||||
// Host key verification
|
// Host key verification
|
||||||
var hostKeyCallback ssh.HostKeyCallback
|
var hostKeyCallback ssh.HostKeyCallback
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
home := env("DIR_HOME")
|
||||||
if err != nil {
|
if home == "" {
|
||||||
return coreerr.E("ssh.Connect", "failed to get user home dir", err)
|
return coreerr.E("ssh.Connect", "failed to get user home dir", nil)
|
||||||
}
|
}
|
||||||
knownHostsPath := filepath.Join(home, ".ssh", "known_hosts")
|
knownHostsPath := joinPath(home, ".ssh", "known_hosts")
|
||||||
|
|
||||||
// Ensure known_hosts file exists
|
// Ensure known_hosts file exists
|
||||||
if !coreio.Local.Exists(knownHostsPath) {
|
if !coreio.Local.Exists(knownHostsPath) {
|
||||||
if err := coreio.Local.EnsureDir(filepath.Dir(knownHostsPath)); err != nil {
|
if err := coreio.Local.EnsureDir(pathDir(knownHostsPath)); err != nil {
|
||||||
return coreerr.E("ssh.Connect", "failed to create .ssh dir", err)
|
return coreerr.E("ssh.Connect", "failed to create .ssh dir", err)
|
||||||
}
|
}
|
||||||
if err := coreio.Local.Write(knownHostsPath, ""); err != nil {
|
if err := coreio.Local.Write(knownHostsPath, ""); err != nil {
|
||||||
|
|
@ -164,19 +160,19 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
||||||
Timeout: c.timeout,
|
Timeout: c.timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
addr := sprintf("%s:%d", c.host, c.port)
|
||||||
|
|
||||||
// Connect with context timeout
|
// Connect with context timeout
|
||||||
var d net.Dialer
|
var d net.Dialer
|
||||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return coreerr.E("ssh.Connect", fmt.Sprintf("dial %s", addr), err)
|
return coreerr.E("ssh.Connect", sprintf("dial %s", addr), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// conn is closed by NewClientConn on error
|
// conn is closed by NewClientConn on error
|
||||||
return coreerr.E("ssh.Connect", fmt.Sprintf("ssh connect %s", addr), err)
|
return coreerr.E("ssh.Connect", sprintf("ssh connect %s", addr), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.client = ssh.NewClient(sshConn, chans, reqs)
|
c.client = ssh.NewClient(sshConn, chans, reqs)
|
||||||
|
|
@ -219,33 +215,33 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string,
|
||||||
becomeUser = "root"
|
becomeUser = "root"
|
||||||
}
|
}
|
||||||
// Escape single quotes in the command
|
// Escape single quotes in the command
|
||||||
escapedCmd := strings.ReplaceAll(cmd, "'", "'\\''")
|
escapedCmd := replaceAll(cmd, "'", "'\\''")
|
||||||
if c.becomePass != "" {
|
if c.becomePass != "" {
|
||||||
// Use sudo with password via stdin (-S flag)
|
// Use sudo with password via stdin (-S flag)
|
||||||
// We launch a goroutine to write the password to stdin
|
// We launch a goroutine to write the password to stdin
|
||||||
cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd)
|
cmd = sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd)
|
||||||
stdin, err := session.StdinPipe()
|
stdin, err := session.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { _ = stdin.Close() }()
|
defer func() { _ = stdin.Close() }()
|
||||||
_, _ = io.WriteString(stdin, c.becomePass+"\n")
|
writeString(stdin, c.becomePass+"\n")
|
||||||
}()
|
}()
|
||||||
} else if c.password != "" {
|
} else if c.password != "" {
|
||||||
// Try using connection password for sudo
|
// Try using connection password for sudo
|
||||||
cmd = fmt.Sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd)
|
cmd = sprintf("sudo -S -u %s bash -c '%s'", becomeUser, escapedCmd)
|
||||||
stdin, err := session.StdinPipe()
|
stdin, err := session.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { _ = stdin.Close() }()
|
defer func() { _ = stdin.Close() }()
|
||||||
_, _ = io.WriteString(stdin, c.password+"\n")
|
writeString(stdin, c.password+"\n")
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
// Try passwordless sudo
|
// Try passwordless sudo
|
||||||
cmd = fmt.Sprintf("sudo -n -u %s bash -c '%s'", becomeUser, escapedCmd)
|
cmd = sprintf("sudo -n -u %s bash -c '%s'", becomeUser, escapedCmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,7 +271,7 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string,
|
||||||
// RunScript runs a script on the remote host.
|
// RunScript runs a script on the remote host.
|
||||||
func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) {
|
func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) {
|
||||||
// Escape the script for heredoc
|
// Escape the script for heredoc
|
||||||
cmd := fmt.Sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script)
|
cmd := sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script)
|
||||||
return c.Run(ctx, cmd)
|
return c.Run(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,23 +282,23 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read content
|
// Read content
|
||||||
content, err := io.ReadAll(local)
|
content, err := readAllString(local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return coreerr.E("ssh.Upload", "read content", err)
|
return coreerr.E("ssh.Upload", "read content", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create parent directory
|
// Create parent directory
|
||||||
dir := filepath.Dir(remote)
|
dir := pathDir(remote)
|
||||||
dirCmd := fmt.Sprintf("mkdir -p %q", dir)
|
dirCmd := sprintf("mkdir -p %q", dir)
|
||||||
if c.become {
|
if c.become {
|
||||||
dirCmd = fmt.Sprintf("sudo mkdir -p %q", dir)
|
dirCmd = sprintf("sudo mkdir -p %q", dir)
|
||||||
}
|
}
|
||||||
if _, _, _, err := c.Run(ctx, dirCmd); err != nil {
|
if _, _, _, err := c.Run(ctx, dirCmd); err != nil {
|
||||||
return coreerr.E("ssh.Upload", "create parent dir", err)
|
return coreerr.E("ssh.Upload", "create parent dir", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use cat to write the file (simpler than SCP)
|
// Use cat to write the file (simpler than SCP)
|
||||||
writeCmd := fmt.Sprintf("cat > %q && chmod %o %q", remote, mode, remote)
|
writeCmd := sprintf("cat > %q && chmod %o %q", remote, mode, remote)
|
||||||
|
|
||||||
// If become is needed, we construct a command that reads password then content from stdin
|
// If become is needed, we construct a command that reads password then content from stdin
|
||||||
// But we need to be careful with handling stdin for sudo + cat.
|
// But we need to be careful with handling stdin for sudo + cat.
|
||||||
|
|
@ -335,11 +331,11 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
||||||
|
|
||||||
if pass != "" {
|
if pass != "" {
|
||||||
// Use sudo -S with password from stdin
|
// Use sudo -S with password from stdin
|
||||||
writeCmd = fmt.Sprintf("sudo -S -u %s bash -c 'cat > %q && chmod %o %q'",
|
writeCmd = sprintf("sudo -S -u %s bash -c 'cat > %q && chmod %o %q'",
|
||||||
becomeUser, remote, mode, remote)
|
becomeUser, remote, mode, remote)
|
||||||
} else {
|
} else {
|
||||||
// Use passwordless sudo (sudo -n) to avoid consuming file content as password
|
// Use passwordless sudo (sudo -n) to avoid consuming file content as password
|
||||||
writeCmd = fmt.Sprintf("sudo -n -u %s bash -c 'cat > %q && chmod %o %q'",
|
writeCmd = sprintf("sudo -n -u %s bash -c 'cat > %q && chmod %o %q'",
|
||||||
becomeUser, remote, mode, remote)
|
becomeUser, remote, mode, remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,9 +346,9 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { _ = stdin.Close() }()
|
defer func() { _ = stdin.Close() }()
|
||||||
if pass != "" {
|
if pass != "" {
|
||||||
_, _ = io.WriteString(stdin, pass+"\n")
|
writeString(stdin, pass+"\n")
|
||||||
}
|
}
|
||||||
_, _ = stdin.Write(content)
|
_, _ = stdin.Write([]byte(content))
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
// Normal write
|
// Normal write
|
||||||
|
|
@ -362,12 +358,12 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { _ = stdin.Close() }()
|
defer func() { _ = stdin.Close() }()
|
||||||
_, _ = stdin.Write(content)
|
_, _ = stdin.Write([]byte(content))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := session2.Wait(); err != nil {
|
if err := session2.Wait(); err != nil {
|
||||||
return coreerr.E("ssh.Upload", fmt.Sprintf("write failed (stderr: %s)", stderrBuf.String()), err)
|
return coreerr.E("ssh.Upload", sprintf("write failed (stderr: %s)", stderrBuf.String()), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -379,14 +375,14 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := fmt.Sprintf("cat %q", remote)
|
cmd := sprintf("cat %q", remote)
|
||||||
|
|
||||||
stdout, stderr, exitCode, err := c.Run(ctx, cmd)
|
stdout, stderr, exitCode, err := c.Run(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
return nil, coreerr.E("ssh.Download", fmt.Sprintf("cat failed: %s", stderr), nil)
|
return nil, coreerr.E("ssh.Download", sprintf("cat failed: %s", stderr), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(stdout), nil
|
return []byte(stdout), nil
|
||||||
|
|
@ -394,7 +390,7 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error)
|
||||||
|
|
||||||
// FileExists checks if a file exists on the remote host.
|
// FileExists checks if a file exists on the remote host.
|
||||||
func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) {
|
func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) {
|
||||||
cmd := fmt.Sprintf("test -e %q && echo yes || echo no", path)
|
cmd := sprintf("test -e %q && echo yes || echo no", path)
|
||||||
stdout, _, exitCode, err := c.Run(ctx, cmd)
|
stdout, _, exitCode, err := c.Run(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -403,13 +399,13 @@ func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) {
|
||||||
// test command failed but didn't error - file doesn't exist
|
// test command failed but didn't error - file doesn't exist
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(stdout) == "yes", nil
|
return corexTrimSpace(stdout) == "yes", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat returns file info from the remote host.
|
// Stat returns file info from the remote host.
|
||||||
func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) {
|
func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) {
|
||||||
// Simple approach - get basic file info
|
// Simple approach - get basic file info
|
||||||
cmd := fmt.Sprintf(`
|
cmd := sprintf(`
|
||||||
if [ -e %q ]; then
|
if [ -e %q ]; then
|
||||||
if [ -d %q ]; then
|
if [ -d %q ]; then
|
||||||
echo "exists=true isdir=true"
|
echo "exists=true isdir=true"
|
||||||
|
|
@ -427,9 +423,9 @@ fi
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]any)
|
result := make(map[string]any)
|
||||||
parts := strings.Fields(strings.TrimSpace(stdout))
|
parts := fields(corexTrimSpace(stdout))
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
kv := strings.SplitN(part, "=", 2)
|
kv := splitN(part, "=", 2)
|
||||||
if len(kv) == 2 {
|
if len(kv) == 2 {
|
||||||
result[kv[0]] = kv[1] == "true"
|
result[kv[0]] = kv[1] == "true"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue