[agent/codex] Upgrade this package to dappco.re/go/core v0.8.0-alpha.1. Ru... #12
10 changed files with 739 additions and 299 deletions
|
|
@ -2,9 +2,6 @@ package anscmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
|
|
@ -16,7 +13,7 @@ import (
|
|||
// args extracts all positional arguments from Options.
|
||||
func args(opts core.Options) []string {
|
||||
var out []string
|
||||
for _, o := range opts {
|
||||
for _, o := range opts.Items() {
|
||||
if o.Key == "_arg" {
|
||||
if s, ok := o.Value.(string); ok {
|
||||
out = append(out, s)
|
||||
|
|
@ -34,16 +31,16 @@ func runAnsible(opts core.Options) core.Result {
|
|||
playbookPath := positional[0]
|
||||
|
||||
// Resolve playbook path
|
||||
if !filepath.IsAbs(playbookPath) {
|
||||
playbookPath, _ = filepath.Abs(playbookPath)
|
||||
if !pathIsAbs(playbookPath) {
|
||||
playbookPath = absPath(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
|
||||
basePath := filepath.Dir(playbookPath)
|
||||
basePath := pathDir(playbookPath)
|
||||
executor := ansible.NewExecutor(basePath)
|
||||
defer executor.Close()
|
||||
|
||||
|
|
@ -53,16 +50,16 @@ func runAnsible(opts core.Options) core.Result {
|
|||
executor.Verbose = opts.Int("verbose")
|
||||
|
||||
if tags := opts.String("tags"); tags != "" {
|
||||
executor.Tags = strings.Split(tags, ",")
|
||||
executor.Tags = split(tags, ",")
|
||||
}
|
||||
if skipTags := opts.String("skip-tags"); skipTags != "" {
|
||||
executor.SkipTags = strings.Split(skipTags, ",")
|
||||
executor.SkipTags = split(skipTags, ",")
|
||||
}
|
||||
|
||||
// Parse extra vars
|
||||
if extraVars := opts.String("extra-vars"); extraVars != "" {
|
||||
for _, v := range strings.Split(extraVars, ",") {
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
for _, v := range split(extraVars, ",") {
|
||||
parts := splitN(v, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
executor.SetVar(parts[0], parts[1])
|
||||
}
|
||||
|
|
@ -71,17 +68,17 @@ func runAnsible(opts core.Options) core.Result {
|
|||
|
||||
// Load inventory
|
||||
if invPath := opts.String("inventory"); invPath != "" {
|
||||
if !filepath.IsAbs(invPath) {
|
||||
invPath, _ = filepath.Abs(invPath)
|
||||
if !pathIsAbs(invPath) {
|
||||
invPath = absPath(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) {
|
||||
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) {
|
||||
invPath = p
|
||||
break
|
||||
|
|
@ -96,8 +93,9 @@ func runAnsible(opts core.Options) core.Result {
|
|||
|
||||
// Set up callbacks
|
||||
executor.OnPlayStart = func(play *ansible.Play) {
|
||||
fmt.Printf("\nPLAY [%s]\n", play.Name)
|
||||
fmt.Println(strings.Repeat("*", 70))
|
||||
print("")
|
||||
print("PLAY [%s]", play.Name)
|
||||
print("%s", repeat("*", 70))
|
||||
}
|
||||
|
||||
executor.OnTaskStart = func(host string, task *ansible.Task) {
|
||||
|
|
@ -105,9 +103,10 @@ func runAnsible(opts core.Options) core.Result {
|
|||
if taskName == "" {
|
||||
taskName = task.Module
|
||||
}
|
||||
fmt.Printf("\nTASK [%s]\n", taskName)
|
||||
print("")
|
||||
print("TASK [%s]", taskName)
|
||||
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"
|
||||
}
|
||||
|
||||
fmt.Printf("%s: [%s]", status, host)
|
||||
line := sprintf("%s: [%s]", status, host)
|
||||
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 {
|
||||
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 != "" {
|
||||
fmt.Printf("%s\n", result.Stderr)
|
||||
print("%s", result.Stderr)
|
||||
}
|
||||
|
||||
if executor.Verbose > 1 {
|
||||
if result.Stdout != "" {
|
||||
fmt.Printf("stdout: %s\n", strings.TrimSpace(result.Stdout))
|
||||
print("stdout: %s", trimSpace(result.Stdout))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executor.OnPlayEnd = func(play *ansible.Play) {
|
||||
fmt.Println()
|
||||
print("")
|
||||
}
|
||||
|
||||
// Run playbook
|
||||
ctx := context.Background()
|
||||
start := time.Now()
|
||||
|
||||
fmt.Printf("Running playbook: %s\n", playbookPath)
|
||||
print("Running playbook: %s", playbookPath)
|
||||
|
||||
if err := executor.Run(ctx, playbookPath); err != nil {
|
||||
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}
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ func runAnsibleTest(opts core.Options) core.Result {
|
|||
}
|
||||
host := positional[0]
|
||||
|
||||
fmt.Printf("Testing SSH connection to %s...\n", host)
|
||||
print("Testing SSH connection to %s...", host)
|
||||
|
||||
cfg := ansible.SSHConfig{
|
||||
Host: host,
|
||||
|
|
@ -194,46 +194,48 @@ func runAnsibleTest(opts core.Options) core.Result {
|
|||
}
|
||||
connectTime := time.Since(start)
|
||||
|
||||
fmt.Printf("Connected in %s\n", connectTime.Round(time.Millisecond))
|
||||
print("Connected in %s", connectTime.Round(time.Millisecond))
|
||||
|
||||
// Gather facts
|
||||
fmt.Println("\nGathering facts...")
|
||||
print("")
|
||||
print("Gathering facts...")
|
||||
|
||||
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")
|
||||
if stdout != "" {
|
||||
fmt.Printf(" OS: %s\n", strings.TrimSpace(stdout))
|
||||
print(" OS: %s", trimSpace(stdout))
|
||||
}
|
||||
|
||||
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")
|
||||
fmt.Printf(" Architecture: %s\n", strings.TrimSpace(stdout))
|
||||
print(" Architecture: %s", trimSpace(stdout))
|
||||
|
||||
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\"}'")
|
||||
fmt.Printf(" Disk: %s\n", strings.TrimSpace(stdout))
|
||||
print(" Disk: %s", trimSpace(stdout))
|
||||
|
||||
stdout, _, _, err = client.Run(ctx, "docker --version 2>/dev/null")
|
||||
if err == nil {
|
||||
fmt.Printf(" Docker: %s\n", strings.TrimSpace(stdout))
|
||||
print(" Docker: %s", trimSpace(stdout))
|
||||
} 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'")
|
||||
if strings.TrimSpace(stdout) == "running" {
|
||||
fmt.Printf(" Coolify: running\n")
|
||||
if trimSpace(stdout) == "running" {
|
||||
print(" Coolify: running")
|
||||
} 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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,25 +9,25 @@ func Register(c *core.Core) {
|
|||
c.Command("ansible", core.Command{
|
||||
Description: "Run Ansible playbooks natively (no Python required)",
|
||||
Action: runAnsible,
|
||||
Flags: core.Options{
|
||||
{Key: "inventory", Value: ""},
|
||||
{Key: "limit", Value: ""},
|
||||
{Key: "tags", Value: ""},
|
||||
{Key: "skip-tags", Value: ""},
|
||||
{Key: "extra-vars", Value: ""},
|
||||
{Key: "verbose", Value: 0},
|
||||
{Key: "check", Value: false},
|
||||
},
|
||||
Flags: core.NewOptions(
|
||||
core.Option{Key: "inventory", Value: ""},
|
||||
core.Option{Key: "limit", Value: ""},
|
||||
core.Option{Key: "tags", Value: ""},
|
||||
core.Option{Key: "skip-tags", Value: ""},
|
||||
core.Option{Key: "extra-vars", Value: ""},
|
||||
core.Option{Key: "verbose", Value: 0},
|
||||
core.Option{Key: "check", Value: false},
|
||||
),
|
||||
})
|
||||
|
||||
c.Command("ansible/test", core.Command{
|
||||
Description: "Test SSH connectivity to a host",
|
||||
Action: runAnsibleTest,
|
||||
Flags: core.Options{
|
||||
{Key: "user", Value: "root"},
|
||||
{Key: "password", Value: ""},
|
||||
{Key: "key", Value: ""},
|
||||
{Key: "port", Value: 22},
|
||||
},
|
||||
Flags: core.NewOptions(
|
||||
core.Option{Key: "user", Value: "root"},
|
||||
core.Option{Key: "password", Value: ""},
|
||||
core.Option{Key: "key", Value: ""},
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
|
@ -86,7 +83,7 @@ func (e *Executor) Run(ctx context.Context, playbookPath string) error {
|
|||
|
||||
for i := range plays {
|
||||
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
|
||||
tasks, err := e.parser.ParseRole(roleRef.Role, roleRef.TasksFrom)
|
||||
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
|
||||
|
|
@ -267,7 +264,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, task *Task, p
|
|||
// Get SSH client
|
||||
client, err := e.getClient(host, play)
|
||||
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
|
||||
|
|
@ -485,7 +482,7 @@ func (e *Executor) getHosts(pattern string) []string {
|
|||
|
||||
var filtered []string
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -582,32 +579,32 @@ func (e *Executor) gatherFacts(ctx context.Context, host string, play *Play) err
|
|||
// Hostname
|
||||
stdout, _, _, err := client.Run(ctx, "hostname -f 2>/dev/null || hostname")
|
||||
if err == nil {
|
||||
facts.FQDN = strings.TrimSpace(stdout)
|
||||
facts.FQDN = corexTrimSpace(stdout)
|
||||
}
|
||||
|
||||
stdout, _, _, err = client.Run(ctx, "hostname -s 2>/dev/null || hostname")
|
||||
if err == nil {
|
||||
facts.Hostname = strings.TrimSpace(stdout)
|
||||
facts.Hostname = corexTrimSpace(stdout)
|
||||
}
|
||||
|
||||
// OS info
|
||||
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") {
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
facts.Distribution = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
|
||||
for _, line := range split(stdout, "\n") {
|
||||
if corexHasPrefix(line, "ID=") {
|
||||
facts.Distribution = trimCutset(corexTrimPrefix(line, "ID="), "\"")
|
||||
}
|
||||
if strings.HasPrefix(line, "VERSION_ID=") {
|
||||
facts.Version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
|
||||
if corexHasPrefix(line, "VERSION_ID=") {
|
||||
facts.Version = trimCutset(corexTrimPrefix(line, "VERSION_ID="), "\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Architecture
|
||||
stdout, _, _, _ = client.Run(ctx, "uname -m")
|
||||
facts.Architecture = strings.TrimSpace(stdout)
|
||||
facts.Architecture = corexTrimSpace(stdout)
|
||||
|
||||
// Kernel
|
||||
stdout, _, _, _ = client.Run(ctx, "uname -r")
|
||||
facts.Kernel = strings.TrimSpace(stdout)
|
||||
facts.Kernel = corexTrimSpace(stdout)
|
||||
|
||||
e.mu.Lock()
|
||||
e.facts[host] = facts
|
||||
|
|
@ -650,11 +647,11 @@ func normalizeConditions(when any) []string {
|
|||
|
||||
// evalCondition evaluates a single condition.
|
||||
func (e *Executor) evalCondition(cond string, host string) bool {
|
||||
cond = strings.TrimSpace(cond)
|
||||
cond = corexTrimSpace(cond)
|
||||
|
||||
// Handle negation
|
||||
if strings.HasPrefix(cond, "not ") {
|
||||
return !e.evalCondition(strings.TrimPrefix(cond, "not "), host)
|
||||
if corexHasPrefix(cond, "not ") {
|
||||
return !e.evalCondition(corexTrimPrefix(cond, "not "), host)
|
||||
}
|
||||
|
||||
// Handle boolean literals
|
||||
|
|
@ -667,10 +664,10 @@ func (e *Executor) evalCondition(cond string, host string) bool {
|
|||
|
||||
// Handle registered variable checks
|
||||
// e.g., "result is success", "result.rc == 0"
|
||||
if strings.Contains(cond, " is ") {
|
||||
parts := strings.SplitN(cond, " is ", 2)
|
||||
varName := strings.TrimSpace(parts[0])
|
||||
check := strings.TrimSpace(parts[1])
|
||||
if contains(cond, " is ") {
|
||||
parts := splitN(cond, " is ", 2)
|
||||
varName := corexTrimSpace(parts[0])
|
||||
check := corexTrimSpace(parts[1])
|
||||
|
||||
result := e.getRegisteredVar(host, varName)
|
||||
if result == nil {
|
||||
|
|
@ -694,7 +691,7 @@ func (e *Executor) evalCondition(cond string, host string) bool {
|
|||
}
|
||||
|
||||
// Handle simple var checks
|
||||
if strings.Contains(cond, " | default(") {
|
||||
if contains(cond, " | default(") {
|
||||
// Extract var name and check if defined
|
||||
re := regexp.MustCompile(`(\w+)\s*\|\s*default\([^)]*\)`)
|
||||
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()
|
||||
|
||||
// Handle dotted access (e.g., "result.stdout")
|
||||
parts := strings.SplitN(name, ".", 2)
|
||||
parts := splitN(name, ".", 2)
|
||||
varName := parts[0]
|
||||
|
||||
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*\}\}`)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
@ -756,20 +753,20 @@ func (e *Executor) templateString(s string, host string, task *Task) string {
|
|||
// resolveExpr resolves a template expression.
|
||||
func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
||||
// Handle filters
|
||||
if strings.Contains(expr, " | ") {
|
||||
parts := strings.SplitN(expr, " | ", 2)
|
||||
if contains(expr, " | ") {
|
||||
parts := splitN(expr, " | ", 2)
|
||||
value := e.resolveExpr(parts[0], host, task)
|
||||
return e.applyFilter(value, parts[1])
|
||||
}
|
||||
|
||||
// Handle lookups
|
||||
if strings.HasPrefix(expr, "lookup(") {
|
||||
if corexHasPrefix(expr, "lookup(") {
|
||||
return e.handleLookup(expr)
|
||||
}
|
||||
|
||||
// Handle registered vars
|
||||
if strings.Contains(expr, ".") {
|
||||
parts := strings.SplitN(expr, ".", 2)
|
||||
if contains(expr, ".") {
|
||||
parts := splitN(expr, ".", 2)
|
||||
if result := e.getRegisteredVar(host, parts[0]); result != nil {
|
||||
switch parts[1] {
|
||||
case "stdout":
|
||||
|
|
@ -777,24 +774,24 @@ func (e *Executor) resolveExpr(expr string, host string, task *Task) string {
|
|||
case "stderr":
|
||||
return result.Stderr
|
||||
case "rc":
|
||||
return fmt.Sprintf("%d", result.RC)
|
||||
return sprintf("%d", result.RC)
|
||||
case "changed":
|
||||
return fmt.Sprintf("%t", result.Changed)
|
||||
return sprintf("%t", result.Changed)
|
||||
case "failed":
|
||||
return fmt.Sprintf("%t", result.Failed)
|
||||
return sprintf("%t", result.Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vars
|
||||
if val, ok := e.vars[expr]; ok {
|
||||
return fmt.Sprintf("%v", val)
|
||||
return sprintf("%v", val)
|
||||
}
|
||||
|
||||
// Check task vars
|
||||
if task != nil {
|
||||
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 {
|
||||
hostVars := GetHostVars(e.inventory, host)
|
||||
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.
|
||||
func (e *Executor) applyFilter(value, filter string) string {
|
||||
filter = strings.TrimSpace(filter)
|
||||
filter = corexTrimSpace(filter)
|
||||
|
||||
// Handle default filter
|
||||
if strings.HasPrefix(filter, "default(") {
|
||||
if corexHasPrefix(filter, "default(") {
|
||||
if value == "" || value == "{{ "+filter+" }}" {
|
||||
// Extract default value
|
||||
re := regexp.MustCompile(`default\(([^)]*)\)`)
|
||||
if match := re.FindStringSubmatch(filter); len(match) > 1 {
|
||||
return strings.Trim(match[1], "'\"")
|
||||
return trimCutset(match[1], "'\"")
|
||||
}
|
||||
}
|
||||
return value
|
||||
|
|
@ -845,8 +842,8 @@ func (e *Executor) applyFilter(value, filter string) string {
|
|||
|
||||
// Handle bool filter
|
||||
if filter == "bool" {
|
||||
lower := strings.ToLower(value)
|
||||
if lower == "true" || lower == "yes" || lower == "1" {
|
||||
lowered := lower(value)
|
||||
if lowered == "true" || lowered == "yes" || lowered == "1" {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
|
|
@ -854,7 +851,7 @@ func (e *Executor) applyFilter(value, filter string) string {
|
|||
|
||||
// Handle trim
|
||||
if filter == "trim" {
|
||||
return strings.TrimSpace(value)
|
||||
return corexTrimSpace(value)
|
||||
}
|
||||
|
||||
// Handle b64decode
|
||||
|
|
@ -880,7 +877,7 @@ func (e *Executor) handleLookup(expr string) string {
|
|||
|
||||
switch lookupType {
|
||||
case "env":
|
||||
return os.Getenv(arg)
|
||||
return env(arg)
|
||||
case "file":
|
||||
if data, err := coreio.Local.Read(arg); err == nil {
|
||||
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)
|
||||
tmplContent := content
|
||||
tmplContent = strings.ReplaceAll(tmplContent, "{{", "{{ .")
|
||||
tmplContent = strings.ReplaceAll(tmplContent, "{%", "{{")
|
||||
tmplContent = strings.ReplaceAll(tmplContent, "%}", "}}")
|
||||
tmplContent = replaceAll(tmplContent, "{{", "{{ .")
|
||||
tmplContent = replaceAll(tmplContent, "{%", "{{")
|
||||
tmplContent = replaceAll(tmplContent, "%}", "}}")
|
||||
|
||||
tmpl, err := template.New("template").Parse(tmplContent)
|
||||
if err != nil {
|
||||
|
|
@ -1010,8 +1007,8 @@ func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) {
|
|||
context["ansible_kernel"] = facts.Kernel
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, context); err != nil {
|
||||
buf := newBuilder()
|
||||
if err := tmpl.Execute(buf, context); err != 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
|
||||
|
||||
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/log v0.1.0
|
||||
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.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
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/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
|
|
|
|||
245
modules.go
245
modules.go
|
|
@ -3,11 +3,8 @@ package ansible
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -135,7 +132,7 @@ func (e *Executor) executeModule(ctx context.Context, host string, client *SSHCl
|
|||
|
||||
default:
|
||||
// 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 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
|
||||
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)
|
||||
|
|
@ -214,7 +211,7 @@ func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args ma
|
|||
|
||||
// Handle 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)
|
||||
|
|
@ -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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle owner/group (best-effort, errors ignored)
|
||||
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 != "" {
|
||||
_, _, _, _ = 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) {
|
||||
|
|
@ -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 {
|
||||
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) {
|
||||
|
|
@ -363,21 +360,21 @@ func (e *Executor) moduleFile(ctx context.Context, client *SSHClient, args map[s
|
|||
switch state {
|
||||
case "directory":
|
||||
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)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "absent":
|
||||
cmd := fmt.Sprintf("rm -rf %q", path)
|
||||
cmd := sprintf("rm -rf %q", path)
|
||||
_, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "touch":
|
||||
cmd := fmt.Sprintf("touch %q", path)
|
||||
cmd := sprintf("touch %q", path)
|
||||
_, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
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 == "" {
|
||||
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)
|
||||
if err != nil || rc != 0 {
|
||||
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":
|
||||
// Ensure file exists and set permissions
|
||||
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)
|
||||
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 != "" {
|
||||
_, _, _, _ = 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 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 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)
|
||||
if rc != 0 {
|
||||
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
|
||||
if regexp != "" {
|
||||
// Replace line matching regexp
|
||||
escapedLine := strings.ReplaceAll(line, "/", "\\/")
|
||||
cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path)
|
||||
escapedLine := replaceAll(line, "/", "\\/")
|
||||
cmd := sprintf("sed -i 's/%s/%s/' %q", regexp, escapedLine, path)
|
||||
_, _, rc, _ := client.Run(ctx, cmd)
|
||||
if rc != 0 {
|
||||
// Line not found, append
|
||||
cmd = fmt.Sprintf("echo %q >> %q", line, path)
|
||||
cmd = sprintf("echo %q >> %q", line, path)
|
||||
_, _, _, _ = client.Run(ctx, cmd)
|
||||
}
|
||||
} else if line != "" {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -512,7 +509,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[
|
|||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -520,7 +517,7 @@ func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[
|
|||
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) {
|
||||
|
|
@ -531,7 +528,7 @@ func (e *Executor) moduleGetURL(ctx context.Context, client *SSHClient, args map
|
|||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || rc != 0 {
|
||||
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)
|
||||
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
|
||||
|
|
@ -561,12 +558,12 @@ func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[st
|
|||
switch state {
|
||||
case "present", "installed":
|
||||
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":
|
||||
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":
|
||||
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 == "" {
|
||||
|
|
@ -588,7 +585,7 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map
|
|||
|
||||
if state == "absent" {
|
||||
if keyring != "" {
|
||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", keyring))
|
||||
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", keyring))
|
||||
}
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
|
@ -599,9 +596,9 @@ func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map
|
|||
|
||||
var cmd string
|
||||
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 {
|
||||
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)
|
||||
|
|
@ -623,19 +620,19 @@ func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, a
|
|||
|
||||
if filename == "" {
|
||||
// Generate filename from repo
|
||||
filename = strings.ReplaceAll(repo, " ", "-")
|
||||
filename = strings.ReplaceAll(filename, "/", "-")
|
||||
filename = strings.ReplaceAll(filename, ":", "")
|
||||
filename = replaceAll(repo, " ", "-")
|
||||
filename = 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" {
|
||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("rm -f %q", path))
|
||||
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", path))
|
||||
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)
|
||||
if err != nil || rc != 0 {
|
||||
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) {
|
||||
// Detect package manager and delegate
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -670,11 +667,11 @@ func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[st
|
|||
var cmd string
|
||||
switch state {
|
||||
case "present", "installed":
|
||||
cmd = fmt.Sprintf("%s install %s", executable, name)
|
||||
cmd = sprintf("%s install %s", executable, name)
|
||||
case "absent", "removed":
|
||||
cmd = fmt.Sprintf("%s uninstall -y %s", executable, name)
|
||||
cmd = sprintf("%s uninstall -y %s", executable, name)
|
||||
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)
|
||||
|
|
@ -701,21 +698,21 @@ func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args ma
|
|||
if state != "" {
|
||||
switch state {
|
||||
case "started":
|
||||
cmds = append(cmds, fmt.Sprintf("systemctl start %s", name))
|
||||
cmds = append(cmds, sprintf("systemctl start %s", name))
|
||||
case "stopped":
|
||||
cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name))
|
||||
cmds = append(cmds, sprintf("systemctl stop %s", name))
|
||||
case "restarted":
|
||||
cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name))
|
||||
cmds = append(cmds, sprintf("systemctl restart %s", name))
|
||||
case "reloaded":
|
||||
cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name))
|
||||
cmds = append(cmds, sprintf("systemctl reload %s", name))
|
||||
}
|
||||
}
|
||||
|
||||
if enabled != nil {
|
||||
if getBoolArg(args, "enabled", false) {
|
||||
cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name))
|
||||
cmds = append(cmds, sprintf("systemctl enable %s", name))
|
||||
} 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" {
|
||||
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)
|
||||
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
|
||||
optsStr := strings.Join(opts, " ")
|
||||
optsStr := join(" ", opts)
|
||||
var cmd string
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -806,7 +803,7 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[
|
|||
}
|
||||
|
||||
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)
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
|
@ -819,8 +816,8 @@ func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[
|
|||
opts = append(opts, "-r")
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
|
||||
name, strings.Join(opts, " "), name)
|
||||
cmd := sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
|
||||
name, join(" ", opts), name)
|
||||
|
||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
|
|
@ -847,7 +844,7 @@ func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[st
|
|||
// Headers
|
||||
if headers, ok := args["headers"].(map[string]any); ok {
|
||||
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
|
||||
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)
|
||||
if err != nil {
|
||||
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
||||
}
|
||||
|
||||
// Parse status code from last line
|
||||
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||
lines := split(corexTrimSpace(stdout), "\n")
|
||||
statusCode := 0
|
||||
if len(lines) > 0 {
|
||||
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) {
|
||||
msg := getStringArg(args, "msg", "")
|
||||
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{
|
||||
|
|
@ -921,7 +918,7 @@ func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult,
|
|||
conditions := normalizeConditions(that)
|
||||
for _, cond := range conditions {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -999,7 +996,7 @@ func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args ma
|
|||
}
|
||||
|
||||
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)
|
||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
|
|
@ -1025,9 +1022,9 @@ func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[st
|
|||
var cmd string
|
||||
if exists {
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -1049,7 +1046,7 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args
|
|||
}
|
||||
|
||||
// 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
|
||||
if !remote {
|
||||
|
|
@ -1058,28 +1055,28 @@ func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args
|
|||
if err != nil {
|
||||
return nil, coreerr.E("Executor.moduleUnarchive", "read src", err)
|
||||
}
|
||||
tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src)
|
||||
err = client.Upload(ctx, strings.NewReader(data), tmpPath, 0644)
|
||||
tmpPath := "/tmp/ansible_unarchive_" + pathBase(src)
|
||||
err = client.Upload(ctx, newReader(data), tmpPath, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") {
|
||||
cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest)
|
||||
} else if strings.HasSuffix(src, ".tar.xz") {
|
||||
cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest)
|
||||
} else if strings.HasSuffix(src, ".tar.bz2") {
|
||||
cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest)
|
||||
} else if strings.HasSuffix(src, ".tar") {
|
||||
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest)
|
||||
} else if strings.HasSuffix(src, ".zip") {
|
||||
cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest)
|
||||
if hasSuffix(src, ".tar.gz") || hasSuffix(src, ".tgz") {
|
||||
cmd = sprintf("tar -xzf %q -C %q", src, dest)
|
||||
} else if hasSuffix(src, ".tar.xz") {
|
||||
cmd = sprintf("tar -xJf %q -C %q", src, dest)
|
||||
} else if hasSuffix(src, ".tar.bz2") {
|
||||
cmd = sprintf("tar -xjf %q -C %q", src, dest)
|
||||
} else if hasSuffix(src, ".tar") {
|
||||
cmd = sprintf("tar -xf %q -C %q", src, dest)
|
||||
} else if hasSuffix(src, ".zip") {
|
||||
cmd = sprintf("unzip -o %q -d %q", src, dest)
|
||||
} 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)
|
||||
|
|
@ -1097,7 +1094,7 @@ func getStringArg(args map[string]any, key, def string) string {
|
|||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
return sprintf("%v", v)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
|
@ -1108,8 +1105,8 @@ func getBoolArg(args map[string]any, key string, def bool) bool {
|
|||
case bool:
|
||||
return b
|
||||
case string:
|
||||
lower := strings.ToLower(b)
|
||||
return lower == "true" || lower == "yes" || lower == "1"
|
||||
lowered := lower(b)
|
||||
return lowered == "true" || lowered == "yes" || lowered == "1"
|
||||
}
|
||||
}
|
||||
return def
|
||||
|
|
@ -1124,14 +1121,14 @@ func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args m
|
|||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
@ -1147,13 +1144,13 @@ func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map
|
|||
|
||||
if state == "absent" {
|
||||
// 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)
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || rc != 0 {
|
||||
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)
|
||||
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)
|
||||
_, _, _, _ = client.Run(ctx, cmd)
|
||||
}
|
||||
|
|
@ -1184,7 +1181,7 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s
|
|||
if state == "absent" {
|
||||
if name != "" {
|
||||
// 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)
|
||||
_, _, _, _ = client.Run(ctx, cmd)
|
||||
}
|
||||
|
|
@ -1192,11 +1189,11 @@ func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[s
|
|||
}
|
||||
|
||||
// Build cron entry
|
||||
schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
|
||||
entry := fmt.Sprintf("%s %s # %s", schedule, job, name)
|
||||
schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
|
||||
entry := sprintf("%s %s # %s", schedule, job, name)
|
||||
|
||||
// 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)
|
||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
|
|
@ -1220,14 +1217,14 @@ func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, arg
|
|||
state := getStringArg(args, "state", "present")
|
||||
create := getBoolArg(args, "create", false)
|
||||
|
||||
beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1)
|
||||
endMarker := strings.Replace(marker, "{mark}", "END", 1)
|
||||
beginMarker := replaceN(marker, "{mark}", "BEGIN", 1)
|
||||
endMarker := replaceN(marker, "{mark}", "END", 1)
|
||||
|
||||
if state == "absent" {
|
||||
// Remove block
|
||||
cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q",
|
||||
strings.ReplaceAll(beginMarker, "/", "\\/"),
|
||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
||||
cmd := sprintf("sed -i '/%s/,/%s/d' %q",
|
||||
replaceAll(beginMarker, "/", "\\/"),
|
||||
replaceAll(endMarker, "/", "\\/"),
|
||||
path)
|
||||
_, _, _, _ = client.Run(ctx, cmd)
|
||||
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)
|
||||
if create {
|
||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("touch %q", path))
|
||||
_, _, _, _ = client.Run(ctx, sprintf("touch %q", path))
|
||||
}
|
||||
|
||||
// Remove existing block and add new one
|
||||
escapedBlock := strings.ReplaceAll(block, "'", "'\\''")
|
||||
cmd := fmt.Sprintf(`
|
||||
escapedBlock := replaceAll(block, "'", "'\\''")
|
||||
cmd := sprintf(`
|
||||
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
|
||||
cat >> %q << 'BLOCK_EOF'
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
BLOCK_EOF
|
||||
`, strings.ReplaceAll(beginMarker, "/", "\\/"),
|
||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
||||
`, replaceAll(beginMarker, "/", "\\/"),
|
||||
replaceAll(endMarker, "/", "\\/"),
|
||||
path, path, beginMarker, escapedBlock, endMarker)
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
} 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
|
||||
|
|
@ -1336,13 +1333,13 @@ func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[st
|
|||
if rule != "" && port != "" {
|
||||
switch rule {
|
||||
case "allow":
|
||||
cmd = fmt.Sprintf("ufw allow %s/%s", port, proto)
|
||||
cmd = sprintf("ufw allow %s/%s", port, proto)
|
||||
case "deny":
|
||||
cmd = fmt.Sprintf("ufw deny %s/%s", port, proto)
|
||||
cmd = sprintf("ufw deny %s/%s", port, proto)
|
||||
case "reject":
|
||||
cmd = fmt.Sprintf("ufw reject %s/%s", port, proto)
|
||||
cmd = sprintf("ufw reject %s/%s", port, proto)
|
||||
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)
|
||||
|
|
@ -1364,11 +1361,11 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return nil, coreerr.E("Executor.moduleAuthorizedKey", "get home dir", err)
|
||||
}
|
||||
home := strings.TrimSpace(stdout)
|
||||
home := corexTrimSpace(stdout)
|
||||
if home == "" {
|
||||
home = "/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" {
|
||||
// Remove key
|
||||
escapedKey := strings.ReplaceAll(key, "/", "\\/")
|
||||
cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
|
||||
escapedKey := replaceAll(key, "/", "\\/")
|
||||
cmd := sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
|
||||
_, _, _, _ = client.Run(ctx, cmd)
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
// Ensure .ssh directory exists (best-effort)
|
||||
_, _, _, _ = client.Run(ctx, fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
|
||||
filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath)))
|
||||
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
|
||||
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
|
||||
|
||||
// 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)
|
||||
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
||||
if err != nil || rc != 0 {
|
||||
|
|
@ -1399,7 +1396,7 @@ func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, a
|
|||
}
|
||||
|
||||
// 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))
|
||||
|
||||
return &TaskResult{Changed: true}, nil
|
||||
|
|
@ -1416,13 +1413,13 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a
|
|||
var cmd string
|
||||
switch state {
|
||||
case "present":
|
||||
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
|
||||
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
|
||||
case "absent":
|
||||
cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc)
|
||||
cmd = sprintf("cd %q && docker compose down", projectSrc)
|
||||
case "restarted":
|
||||
cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc)
|
||||
cmd = sprintf("cd %q && docker compose restart", projectSrc)
|
||||
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)
|
||||
|
|
@ -1431,7 +1428,7 @@ func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, a
|
|||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
41
parser.go
41
parser.go
|
|
@ -1,12 +1,9 @@
|
|||
package ansible
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -42,7 +39,7 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) {
|
|||
// Process each play
|
||||
for i := range plays {
|
||||
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 {
|
||||
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)
|
||||
searchPaths := []string{
|
||||
// Relative to playbook
|
||||
filepath.Join(p.basePath, "roles", name, "tasks", tasksFrom),
|
||||
joinPath(p.basePath, "roles", name, "tasks", tasksFrom),
|
||||
// Parent directory roles
|
||||
filepath.Join(filepath.Dir(p.basePath), "roles", name, "tasks", tasksFrom),
|
||||
joinPath(pathDir(p.basePath), "roles", name, "tasks", tasksFrom),
|
||||
// Sibling roles directory
|
||||
filepath.Join(p.basePath, "..", "roles", name, "tasks", tasksFrom),
|
||||
joinPath(p.basePath, "..", "roles", name, "tasks", tasksFrom),
|
||||
// playbooks/roles pattern
|
||||
filepath.Join(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom),
|
||||
joinPath(p.basePath, "playbooks", "roles", name, "tasks", tasksFrom),
|
||||
// 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
|
||||
for _, sp := range searchPaths {
|
||||
// Clean the path to resolve .. segments
|
||||
sp = filepath.Clean(sp)
|
||||
sp = cleanPath(sp)
|
||||
if coreio.Local.Exists(sp) {
|
||||
tasksPath = sp
|
||||
break
|
||||
|
|
@ -146,11 +143,11 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
|
|||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
var defaults map[string]any
|
||||
if yaml.Unmarshal([]byte(data), &defaults) == nil {
|
||||
|
|
@ -163,7 +160,7 @@ func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
var roleVars map[string]any
|
||||
if yaml.Unmarshal([]byte(data), &roleVars) == nil {
|
||||
|
|
@ -185,25 +182,25 @@ func (p *Parser) processPlay(play *Play) error {
|
|||
|
||||
for i := range play.PreTasks {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
// Also check without ansible.builtin. prefix
|
||||
if strings.HasPrefix(m, "ansible.builtin.") {
|
||||
if key == strings.TrimPrefix(m, "ansible.builtin.") {
|
||||
if corexHasPrefix(m, "ansible.builtin.") {
|
||||
if key == corexTrimPrefix(m, "ansible.builtin.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Accept any key with dots (likely a module)
|
||||
return strings.Contains(key, ".")
|
||||
return contains(key, ".")
|
||||
}
|
||||
|
||||
// NormalizeModule normalizes a module name to its canonical form.
|
||||
func NormalizeModule(name string) string {
|
||||
// Add ansible.builtin. prefix if missing
|
||||
if !strings.Contains(name, ".") {
|
||||
if !contains(name, ".") {
|
||||
return "ansible.builtin." + name
|
||||
}
|
||||
return name
|
||||
|
|
|
|||
80
ssh.go
80
ssh.go
|
|
@ -3,12 +3,9 @@ package ansible
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -87,9 +84,8 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
|||
// Try key-based auth first
|
||||
if c.keyFile != "" {
|
||||
keyPath := c.keyFile
|
||||
if strings.HasPrefix(keyPath, "~") {
|
||||
home, _ := os.UserHomeDir()
|
||||
keyPath = filepath.Join(home, keyPath[1:])
|
||||
if corexHasPrefix(keyPath, "~") {
|
||||
keyPath = joinPath(env("DIR_HOME"), keyPath[1:])
|
||||
}
|
||||
|
||||
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
|
||||
if len(authMethods) == 0 {
|
||||
home, _ := os.UserHomeDir()
|
||||
home := env("DIR_HOME")
|
||||
defaultKeys := []string{
|
||||
filepath.Join(home, ".ssh", "id_ed25519"),
|
||||
filepath.Join(home, ".ssh", "id_rsa"),
|
||||
joinPath(home, ".ssh", "id_ed25519"),
|
||||
joinPath(home, ".ssh", "id_rsa"),
|
||||
}
|
||||
for _, keyPath := range defaultKeys {
|
||||
if key, err := coreio.Local.Read(keyPath); err == nil {
|
||||
|
|
@ -135,15 +131,15 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
|||
// Host key verification
|
||||
var hostKeyCallback ssh.HostKeyCallback
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return coreerr.E("ssh.Connect", "failed to get user home dir", err)
|
||||
home := env("DIR_HOME")
|
||||
if home == "" {
|
||||
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
|
||||
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)
|
||||
}
|
||||
if err := coreio.Local.Write(knownHostsPath, ""); err != nil {
|
||||
|
|
@ -164,19 +160,19 @@ func (c *SSHClient) Connect(ctx context.Context) error {
|
|||
Timeout: c.timeout,
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
||||
addr := sprintf("%s:%d", c.host, c.port)
|
||||
|
||||
// Connect with context timeout
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||
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)
|
||||
if err != nil {
|
||||
// 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)
|
||||
|
|
@ -219,33 +215,33 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string,
|
|||
becomeUser = "root"
|
||||
}
|
||||
// Escape single quotes in the command
|
||||
escapedCmd := strings.ReplaceAll(cmd, "'", "'\\''")
|
||||
escapedCmd := replaceAll(cmd, "'", "'\\''")
|
||||
if c.becomePass != "" {
|
||||
// Use sudo with password via stdin (-S flag)
|
||||
// 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()
|
||||
if err != nil {
|
||||
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
||||
}
|
||||
go func() {
|
||||
defer func() { _ = stdin.Close() }()
|
||||
_, _ = io.WriteString(stdin, c.becomePass+"\n")
|
||||
writeString(stdin, c.becomePass+"\n")
|
||||
}()
|
||||
} else if c.password != "" {
|
||||
// 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()
|
||||
if err != nil {
|
||||
return "", "", -1, coreerr.E("ssh.Run", "stdin pipe", err)
|
||||
}
|
||||
go func() {
|
||||
defer func() { _ = stdin.Close() }()
|
||||
_, _ = io.WriteString(stdin, c.password+"\n")
|
||||
writeString(stdin, c.password+"\n")
|
||||
}()
|
||||
} else {
|
||||
// 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.
|
||||
func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
@ -286,23 +282,23 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
|||
}
|
||||
|
||||
// Read content
|
||||
content, err := io.ReadAll(local)
|
||||
content, err := readAllString(local)
|
||||
if err != nil {
|
||||
return coreerr.E("ssh.Upload", "read content", err)
|
||||
}
|
||||
|
||||
// Create parent directory
|
||||
dir := filepath.Dir(remote)
|
||||
dirCmd := fmt.Sprintf("mkdir -p %q", dir)
|
||||
dir := pathDir(remote)
|
||||
dirCmd := sprintf("mkdir -p %q", dir)
|
||||
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 {
|
||||
return coreerr.E("ssh.Upload", "create parent dir", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 != "" {
|
||||
// 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)
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
@ -350,9 +346,9 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
|||
go func() {
|
||||
defer func() { _ = stdin.Close() }()
|
||||
if pass != "" {
|
||||
_, _ = io.WriteString(stdin, pass+"\n")
|
||||
writeString(stdin, pass+"\n")
|
||||
}
|
||||
_, _ = stdin.Write(content)
|
||||
_, _ = stdin.Write([]byte(content))
|
||||
}()
|
||||
} else {
|
||||
// Normal write
|
||||
|
|
@ -362,12 +358,12 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
|
|||
|
||||
go func() {
|
||||
defer func() { _ = stdin.Close() }()
|
||||
_, _ = stdin.Write(content)
|
||||
_, _ = stdin.Write([]byte(content))
|
||||
}()
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -379,14 +375,14 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("cat %q", remote)
|
||||
cmd := sprintf("cat %q", remote)
|
||||
|
||||
stdout, stderr, exitCode, err := c.Run(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
|
|
@ -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.
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
return false, nil
|
||||
}
|
||||
return strings.TrimSpace(stdout) == "yes", nil
|
||||
return corexTrimSpace(stdout) == "yes", nil
|
||||
}
|
||||
|
||||
// Stat returns file info from the remote host.
|
||||
func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) {
|
||||
// Simple approach - get basic file info
|
||||
cmd := fmt.Sprintf(`
|
||||
cmd := sprintf(`
|
||||
if [ -e %q ]; then
|
||||
if [ -d %q ]; then
|
||||
echo "exists=true isdir=true"
|
||||
|
|
@ -427,9 +423,9 @@ fi
|
|||
}
|
||||
|
||||
result := make(map[string]any)
|
||||
parts := strings.Fields(strings.TrimSpace(stdout))
|
||||
parts := fields(corexTrimSpace(stdout))
|
||||
for _, part := range parts {
|
||||
kv := strings.SplitN(part, "=", 2)
|
||||
kv := splitN(part, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
result[kv[0]] = kv[1] == "true"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue