1574 lines
44 KiB
Go
1574 lines
44 KiB
Go
package ansible
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"io/fs"
|
|
"strconv"
|
|
|
|
coreerr "dappco.re/go/core/log"
|
|
)
|
|
|
|
// executeModule dispatches to the appropriate module handler.
|
|
func (e *Executor) executeModule(ctx context.Context, host string, client *SSHClient, task *Task, play *Play) (*TaskResult, error) {
|
|
module := NormalizeModule(task.Module)
|
|
|
|
// Apply task-level become
|
|
if task.Become != nil && *task.Become {
|
|
// Save old state to restore
|
|
oldBecome := client.become
|
|
oldUser := client.becomeUser
|
|
oldPass := client.becomePass
|
|
|
|
client.SetBecome(true, task.BecomeUser, "")
|
|
|
|
defer client.SetBecome(oldBecome, oldUser, oldPass)
|
|
}
|
|
|
|
e.vars["inventory_hostname"] = host
|
|
|
|
// Apply play/task environment overrides for the duration of the module run.
|
|
if environment := e.taskEnvironment(host, play, task); len(environment) > 0 {
|
|
oldEnvironment := client.Environment()
|
|
client.SetEnvironment(environment)
|
|
defer client.SetEnvironment(oldEnvironment)
|
|
}
|
|
|
|
// Template the args
|
|
args := e.templateArgs(task.Args, host, task)
|
|
|
|
switch module {
|
|
// Command execution
|
|
case "ansible.builtin.shell":
|
|
return e.moduleShell(ctx, client, args)
|
|
case "ansible.builtin.command":
|
|
return e.moduleCommand(ctx, client, args)
|
|
case "ansible.builtin.raw":
|
|
return e.moduleRaw(ctx, client, args)
|
|
case "ansible.builtin.script":
|
|
return e.moduleScript(ctx, client, args)
|
|
|
|
// File operations
|
|
case "ansible.builtin.copy":
|
|
return e.moduleCopy(ctx, client, args, host, task)
|
|
case "ansible.builtin.template":
|
|
return e.moduleTemplate(ctx, client, args, host, task)
|
|
case "ansible.builtin.file":
|
|
return e.moduleFile(ctx, client, args)
|
|
case "ansible.builtin.lineinfile":
|
|
return e.moduleLineinfile(ctx, client, args)
|
|
case "ansible.builtin.stat":
|
|
return e.moduleStat(ctx, client, args)
|
|
case "ansible.builtin.slurp":
|
|
return e.moduleSlurp(ctx, client, args)
|
|
case "ansible.builtin.fetch":
|
|
return e.moduleFetch(ctx, client, args)
|
|
case "ansible.builtin.get_url":
|
|
return e.moduleGetURL(ctx, client, args)
|
|
|
|
// Package management
|
|
case "ansible.builtin.apt":
|
|
return e.moduleApt(ctx, client, args)
|
|
case "ansible.builtin.apt_key":
|
|
return e.moduleAptKey(ctx, client, args)
|
|
case "ansible.builtin.apt_repository":
|
|
return e.moduleAptRepository(ctx, client, args)
|
|
case "ansible.builtin.package":
|
|
return e.modulePackage(ctx, client, args)
|
|
case "ansible.builtin.pip":
|
|
return e.modulePip(ctx, client, args)
|
|
|
|
// Service management
|
|
case "ansible.builtin.service":
|
|
return e.moduleService(ctx, client, args)
|
|
case "ansible.builtin.systemd":
|
|
return e.moduleSystemd(ctx, client, args)
|
|
|
|
// User/Group
|
|
case "ansible.builtin.user":
|
|
return e.moduleUser(ctx, client, args)
|
|
case "ansible.builtin.group":
|
|
return e.moduleGroup(ctx, client, args)
|
|
|
|
// HTTP
|
|
case "ansible.builtin.uri":
|
|
return e.moduleURI(ctx, client, args)
|
|
|
|
// Misc
|
|
case "ansible.builtin.debug":
|
|
return e.moduleDebug(args)
|
|
case "ansible.builtin.fail":
|
|
return e.moduleFail(args)
|
|
case "ansible.builtin.assert":
|
|
return e.moduleAssert(args, host)
|
|
case "ansible.builtin.set_fact":
|
|
return e.moduleSetFact(args)
|
|
case "ansible.builtin.pause":
|
|
return e.modulePause(ctx, args)
|
|
case "ansible.builtin.wait_for":
|
|
return e.moduleWaitFor(ctx, client, args)
|
|
case "ansible.builtin.git":
|
|
return e.moduleGit(ctx, client, args)
|
|
case "ansible.builtin.unarchive":
|
|
return e.moduleUnarchive(ctx, client, args)
|
|
|
|
// Additional modules
|
|
case "ansible.builtin.hostname":
|
|
return e.moduleHostname(ctx, client, args)
|
|
case "ansible.builtin.sysctl":
|
|
return e.moduleSysctl(ctx, client, args)
|
|
case "ansible.builtin.cron":
|
|
return e.moduleCron(ctx, client, args)
|
|
case "ansible.builtin.blockinfile":
|
|
return e.moduleBlockinfile(ctx, client, args)
|
|
case "ansible.builtin.include_vars":
|
|
return e.moduleIncludeVars(args)
|
|
case "ansible.builtin.add_host":
|
|
return e.moduleAddHost(args)
|
|
case "ansible.builtin.meta":
|
|
return e.moduleMeta(args)
|
|
case "ansible.builtin.setup":
|
|
return e.moduleSetup(ctx, client)
|
|
case "ansible.builtin.reboot":
|
|
return e.moduleReboot(ctx, client, args)
|
|
|
|
// Community modules (basic support)
|
|
case "community.general.ufw":
|
|
return e.moduleUFW(ctx, client, args)
|
|
case "ansible.posix.authorized_key":
|
|
return e.moduleAuthorizedKey(ctx, client, args)
|
|
case "community.docker.docker_compose":
|
|
return e.moduleDockerCompose(ctx, client, args)
|
|
|
|
default:
|
|
// For unknown modules, try to execute as shell if it looks like a command
|
|
if contains(task.Module, " ") || task.Module == "" {
|
|
return e.moduleShell(ctx, client, args)
|
|
}
|
|
return nil, coreerr.E("Executor.executeModule", "unsupported module: "+module, nil)
|
|
}
|
|
}
|
|
|
|
func (e *Executor) taskEnvironment(host string, play *Play, task *Task) map[string]string {
|
|
var hasEnvironment bool
|
|
environment := make(map[string]string)
|
|
|
|
if play != nil {
|
|
for key, value := range play.Environment {
|
|
environment[key] = e.templateString(value, host, task)
|
|
hasEnvironment = true
|
|
}
|
|
}
|
|
|
|
if task != nil {
|
|
for key, value := range task.Environment {
|
|
environment[key] = e.templateString(value, host, task)
|
|
hasEnvironment = true
|
|
}
|
|
}
|
|
|
|
if !hasEnvironment {
|
|
return nil
|
|
}
|
|
|
|
return environment
|
|
}
|
|
|
|
// templateArgs templates all string values in args.
|
|
func (e *Executor) templateArgs(args map[string]any, host string, task *Task) map[string]any {
|
|
// Set inventory_hostname for templating
|
|
e.vars["inventory_hostname"] = host
|
|
|
|
result := make(map[string]any)
|
|
for k, v := range args {
|
|
switch val := v.(type) {
|
|
case string:
|
|
result[k] = e.templateString(val, host, task)
|
|
case map[string]any:
|
|
// Recurse for nested maps
|
|
result[k] = e.templateArgs(val, host, task)
|
|
case []any:
|
|
// Template strings in arrays
|
|
templated := make([]any, len(val))
|
|
for i, item := range val {
|
|
if s, ok := item.(string); ok {
|
|
templated[i] = e.templateString(s, host, task)
|
|
} else {
|
|
templated[i] = item
|
|
}
|
|
}
|
|
result[k] = templated
|
|
default:
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- Command Modules ---
|
|
|
|
func (e *Executor) moduleShell(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
cmd := getStringArg(args, "_raw_params", "")
|
|
if cmd == "" {
|
|
cmd = getStringArg(args, "cmd", "")
|
|
}
|
|
if cmd == "" {
|
|
return nil, coreerr.E("Executor.moduleShell", "no command specified", nil)
|
|
}
|
|
|
|
// Handle chdir
|
|
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
|
cmd = sprintf("cd %q && %s", chdir, cmd)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
|
if err != nil {
|
|
return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: true,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
RC: rc,
|
|
Failed: rc != 0,
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleCommand(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
cmd := getStringArg(args, "_raw_params", "")
|
|
if cmd == "" {
|
|
cmd = getStringArg(args, "cmd", "")
|
|
}
|
|
if cmd == "" {
|
|
return nil, coreerr.E("Executor.moduleCommand", "no command specified", nil)
|
|
}
|
|
|
|
// Handle chdir
|
|
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
|
cmd = sprintf("cd %q && %s", chdir, cmd)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil {
|
|
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: true,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
RC: rc,
|
|
Failed: rc != 0,
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleRaw(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
cmd := getStringArg(args, "_raw_params", "")
|
|
if cmd == "" {
|
|
return nil, coreerr.E("Executor.moduleRaw", "no command specified", nil)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil {
|
|
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: true,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
RC: rc,
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleScript(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
script := getStringArg(args, "_raw_params", "")
|
|
if script == "" {
|
|
return nil, coreerr.E("Executor.moduleScript", "no script specified", nil)
|
|
}
|
|
|
|
// Read local script
|
|
data, err := localFS.Read(script)
|
|
if err != nil {
|
|
return nil, coreerr.E("Executor.moduleScript", "read script", err)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.RunScript(ctx, data)
|
|
if err != nil {
|
|
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: true,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
RC: rc,
|
|
Failed: rc != 0,
|
|
}, nil
|
|
}
|
|
|
|
// --- File Modules ---
|
|
|
|
func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
|
dest := getStringArg(args, "dest", "")
|
|
if dest == "" {
|
|
return nil, coreerr.E("Executor.moduleCopy", "dest required", nil)
|
|
}
|
|
|
|
var content string
|
|
var err error
|
|
|
|
if src := getStringArg(args, "src", ""); src != "" {
|
|
content, err = localFS.Read(src)
|
|
if err != nil {
|
|
return nil, coreerr.E("Executor.moduleCopy", "read src", err)
|
|
}
|
|
} else if c := getStringArg(args, "content", ""); c != "" {
|
|
content = c
|
|
} else {
|
|
return nil, coreerr.E("Executor.moduleCopy", "src or content required", nil)
|
|
}
|
|
|
|
mode := fs.FileMode(0644)
|
|
if m := getStringArg(args, "mode", ""); m != "" {
|
|
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
|
|
mode = fs.FileMode(parsed)
|
|
}
|
|
}
|
|
|
|
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, sprintf("chown %s %q", owner, dest))
|
|
}
|
|
if group := getStringArg(args, "group", ""); group != "" {
|
|
_, _, _, _ = client.Run(ctx, sprintf("chgrp %s %q", group, dest))
|
|
}
|
|
|
|
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) {
|
|
src := getStringArg(args, "src", "")
|
|
dest := getStringArg(args, "dest", "")
|
|
if src == "" || dest == "" {
|
|
return nil, coreerr.E("Executor.moduleTemplate", "src and dest required", nil)
|
|
}
|
|
|
|
// Process template
|
|
content, err := e.TemplateFile(src, host, task)
|
|
if err != nil {
|
|
return nil, coreerr.E("Executor.moduleTemplate", "template", err)
|
|
}
|
|
|
|
mode := fs.FileMode(0644)
|
|
if m := getStringArg(args, "mode", ""); m != "" {
|
|
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
|
|
mode = fs.FileMode(parsed)
|
|
}
|
|
}
|
|
|
|
err = client.Upload(ctx, newReader(content), dest, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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) {
|
|
path := getStringArg(args, "path", "")
|
|
if path == "" {
|
|
path = getStringArg(args, "dest", "")
|
|
}
|
|
if path == "" {
|
|
return nil, coreerr.E("Executor.moduleFile", "path required", nil)
|
|
}
|
|
|
|
state := getStringArg(args, "state", "file")
|
|
|
|
switch state {
|
|
case "directory":
|
|
mode := getStringArg(args, "mode", "0755")
|
|
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 := 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 := 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
|
|
}
|
|
|
|
case "link":
|
|
src := getStringArg(args, "src", "")
|
|
if src == "" {
|
|
return nil, coreerr.E("Executor.moduleFile", "src required for link state", nil)
|
|
}
|
|
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
|
|
}
|
|
|
|
case "file":
|
|
// Ensure file exists and set permissions
|
|
if mode := getStringArg(args, "mode", ""); mode != "" {
|
|
_, _, _, _ = 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, sprintf("chown %s %q", owner, path))
|
|
}
|
|
if group := getStringArg(args, "group", ""); group != "" {
|
|
_, _, _, _ = 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, sprintf("chown -R %s %q", owner, path))
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleLineinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
path := getStringArg(args, "path", "")
|
|
if path == "" {
|
|
path = getStringArg(args, "dest", "")
|
|
}
|
|
if path == "" {
|
|
return nil, coreerr.E("Executor.moduleLineinfile", "path required", nil)
|
|
}
|
|
|
|
line := getStringArg(args, "line", "")
|
|
regexp := getStringArg(args, "regexp", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if state == "absent" {
|
|
if regexp != "" {
|
|
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
|
|
}
|
|
}
|
|
} else {
|
|
// state == present
|
|
if regexp != "" {
|
|
// Replace line matching regexp
|
|
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 = sprintf("echo %q >> %q", line, path)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
}
|
|
} else if line != "" {
|
|
// Ensure line is present
|
|
cmd := sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleStat(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
path := getStringArg(args, "path", "")
|
|
if path == "" {
|
|
return nil, coreerr.E("Executor.moduleStat", "path required", nil)
|
|
}
|
|
|
|
stat, err := client.Stat(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: false,
|
|
Data: map[string]any{"stat": stat},
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleSlurp(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
path := getStringArg(args, "path", "")
|
|
if path == "" {
|
|
path = getStringArg(args, "src", "")
|
|
}
|
|
if path == "" {
|
|
return nil, coreerr.E("Executor.moduleSlurp", "path required", nil)
|
|
}
|
|
|
|
content, err := client.Download(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encoded := base64.StdEncoding.EncodeToString(content)
|
|
|
|
return &TaskResult{
|
|
Changed: false,
|
|
Data: map[string]any{"content": encoded, "encoding": "base64"},
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleFetch(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
src := getStringArg(args, "src", "")
|
|
dest := getStringArg(args, "dest", "")
|
|
if src == "" || dest == "" {
|
|
return nil, coreerr.E("Executor.moduleFetch", "src and dest required", nil)
|
|
}
|
|
|
|
content, err := client.Download(ctx, src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create dest directory
|
|
if err := localFS.EnsureDir(pathDir(dest)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := localFS.Write(dest, string(content)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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) {
|
|
url := getStringArg(args, "url", "")
|
|
dest := getStringArg(args, "dest", "")
|
|
if url == "" || dest == "" {
|
|
return nil, coreerr.E("Executor.moduleGetURL", "url and dest required", nil)
|
|
}
|
|
|
|
// Use curl or wget
|
|
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
|
|
}
|
|
|
|
// Set mode if specified (best-effort)
|
|
if mode := getStringArg(args, "mode", ""); mode != "" {
|
|
_, _, _, _ = client.Run(ctx, sprintf("chmod %s %q", mode, dest))
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// --- Package Modules ---
|
|
|
|
func (e *Executor) moduleApt(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
state := getStringArg(args, "state", "present")
|
|
updateCache := getBoolArg(args, "update_cache", false)
|
|
|
|
var cmd string
|
|
|
|
if updateCache {
|
|
_, _, _, _ = client.Run(ctx, "apt-get update -qq")
|
|
}
|
|
|
|
switch state {
|
|
case "present", "installed":
|
|
if name != "" {
|
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
|
|
}
|
|
case "absent", "removed":
|
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
|
|
case "latest":
|
|
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
|
|
}
|
|
|
|
if cmd == "" {
|
|
return &TaskResult{Changed: false}, nil
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleAptKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
url := getStringArg(args, "url", "")
|
|
keyring := getStringArg(args, "keyring", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if state == "absent" {
|
|
if keyring != "" {
|
|
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", keyring))
|
|
}
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
if url == "" {
|
|
return nil, coreerr.E("Executor.moduleAptKey", "url required", nil)
|
|
}
|
|
|
|
var cmd string
|
|
if keyring != "" {
|
|
cmd = sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
|
|
} else {
|
|
cmd = sprintf("curl -fsSL %q | apt-key add -", 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
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleAptRepository(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
repo := getStringArg(args, "repo", "")
|
|
filename := getStringArg(args, "filename", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if repo == "" {
|
|
return nil, coreerr.E("Executor.moduleAptRepository", "repo required", nil)
|
|
}
|
|
|
|
if filename == "" {
|
|
// Generate filename from repo
|
|
filename = replaceAll(repo, " ", "-")
|
|
filename = replaceAll(filename, "/", "-")
|
|
filename = replaceAll(filename, ":", "")
|
|
}
|
|
|
|
path := sprintf("/etc/apt/sources.list.d/%s.list", filename)
|
|
|
|
if state == "absent" {
|
|
_, _, _, _ = client.Run(ctx, sprintf("rm -f %q", path))
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Update apt cache (best-effort)
|
|
if getBoolArg(args, "update_cache", true) {
|
|
_, _, _, _ = client.Run(ctx, "apt-get update -qq")
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
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 = corexTrimSpace(stdout)
|
|
|
|
if contains(stdout, "apt") {
|
|
return e.moduleApt(ctx, client, args)
|
|
}
|
|
|
|
// Default to apt
|
|
return e.moduleApt(ctx, client, args)
|
|
}
|
|
|
|
func (e *Executor) modulePip(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
state := getStringArg(args, "state", "present")
|
|
executable := getStringArg(args, "executable", "pip3")
|
|
|
|
var cmd string
|
|
switch state {
|
|
case "present", "installed":
|
|
cmd = sprintf("%s install %s", executable, name)
|
|
case "absent", "removed":
|
|
cmd = sprintf("%s uninstall -y %s", executable, name)
|
|
case "latest":
|
|
cmd = sprintf("%s install --upgrade %s", executable, 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
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// --- Service Modules ---
|
|
|
|
func (e *Executor) moduleService(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
state := getStringArg(args, "state", "")
|
|
enabled := args["enabled"]
|
|
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleService", "name required", nil)
|
|
}
|
|
|
|
var cmds []string
|
|
|
|
if state != "" {
|
|
switch state {
|
|
case "started":
|
|
cmds = append(cmds, sprintf("systemctl start %s", name))
|
|
case "stopped":
|
|
cmds = append(cmds, sprintf("systemctl stop %s", name))
|
|
case "restarted":
|
|
cmds = append(cmds, sprintf("systemctl restart %s", name))
|
|
case "reloaded":
|
|
cmds = append(cmds, sprintf("systemctl reload %s", name))
|
|
}
|
|
}
|
|
|
|
if enabled != nil {
|
|
if getBoolArg(args, "enabled", false) {
|
|
cmds = append(cmds, sprintf("systemctl enable %s", name))
|
|
} else {
|
|
cmds = append(cmds, sprintf("systemctl disable %s", name))
|
|
}
|
|
}
|
|
|
|
for _, cmd := range cmds {
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: len(cmds) > 0}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleSystemd(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
// systemd is similar to service
|
|
if getBoolArg(args, "daemon_reload", false) {
|
|
_, _, _, _ = client.Run(ctx, "systemctl daemon-reload")
|
|
}
|
|
|
|
return e.moduleService(ctx, client, args)
|
|
}
|
|
|
|
// --- User/Group Modules ---
|
|
|
|
func (e *Executor) moduleUser(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleUser", "name required", nil)
|
|
}
|
|
|
|
if state == "absent" {
|
|
cmd := sprintf("userdel -r %s 2>/dev/null || true", name)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// Build useradd/usermod command
|
|
var opts []string
|
|
|
|
if uid := getStringArg(args, "uid", ""); uid != "" {
|
|
opts = append(opts, "-u", uid)
|
|
}
|
|
if group := getStringArg(args, "group", ""); group != "" {
|
|
opts = append(opts, "-g", group)
|
|
}
|
|
if groups := getStringArg(args, "groups", ""); groups != "" {
|
|
opts = append(opts, "-G", groups)
|
|
}
|
|
if home := getStringArg(args, "home", ""); home != "" {
|
|
opts = append(opts, "-d", home)
|
|
}
|
|
if shell := getStringArg(args, "shell", ""); shell != "" {
|
|
opts = append(opts, "-s", shell)
|
|
}
|
|
if getBoolArg(args, "system", false) {
|
|
opts = append(opts, "-r")
|
|
}
|
|
if getBoolArg(args, "create_home", true) {
|
|
opts = append(opts, "-m")
|
|
}
|
|
|
|
// Try usermod first, then useradd
|
|
optsStr := join(" ", opts)
|
|
var cmd string
|
|
if optsStr == "" {
|
|
cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
|
|
} else {
|
|
cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
|
|
name, optsStr, name, optsStr, 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
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleGroup(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleGroup", "name required", nil)
|
|
}
|
|
|
|
if state == "absent" {
|
|
cmd := sprintf("groupdel %s 2>/dev/null || true", name)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
var opts []string
|
|
if gid := getStringArg(args, "gid", ""); gid != "" {
|
|
opts = append(opts, "-g", gid)
|
|
}
|
|
if getBoolArg(args, "system", false) {
|
|
opts = append(opts, "-r")
|
|
}
|
|
|
|
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 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// --- HTTP Module ---
|
|
|
|
func (e *Executor) moduleURI(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
url := getStringArg(args, "url", "")
|
|
method := getStringArg(args, "method", "GET")
|
|
|
|
if url == "" {
|
|
return nil, coreerr.E("Executor.moduleURI", "url required", nil)
|
|
}
|
|
|
|
var curlOpts []string
|
|
curlOpts = append(curlOpts, "-s", "-S")
|
|
curlOpts = append(curlOpts, "-X", method)
|
|
|
|
// Headers
|
|
if headers, ok := args["headers"].(map[string]any); ok {
|
|
for k, v := range headers {
|
|
curlOpts = append(curlOpts, "-H", sprintf("%s: %v", k, v))
|
|
}
|
|
}
|
|
|
|
// Body
|
|
if body := getStringArg(args, "body", ""); body != "" {
|
|
curlOpts = append(curlOpts, "-d", body)
|
|
}
|
|
|
|
// Status code
|
|
curlOpts = append(curlOpts, "-w", "\\n%{http_code}")
|
|
|
|
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 := split(corexTrimSpace(stdout), "\n")
|
|
statusCode := 0
|
|
if len(lines) > 0 {
|
|
statusCode, _ = strconv.Atoi(lines[len(lines)-1])
|
|
}
|
|
|
|
// Check expected status
|
|
expectedStatus := 200
|
|
if s, ok := args["status_code"].(int); ok {
|
|
expectedStatus = s
|
|
}
|
|
|
|
failed := rc != 0 || statusCode != expectedStatus
|
|
|
|
return &TaskResult{
|
|
Changed: false,
|
|
Failed: failed,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
RC: statusCode,
|
|
Data: map[string]any{"status": statusCode},
|
|
}, nil
|
|
}
|
|
|
|
// --- Misc Modules ---
|
|
|
|
func (e *Executor) moduleDebug(args map[string]any) (*TaskResult, error) {
|
|
msg := getStringArg(args, "msg", "")
|
|
if v, ok := args["var"]; ok {
|
|
msg = sprintf("%v = %v", v, e.vars[sprintf("%v", v)])
|
|
}
|
|
|
|
return &TaskResult{
|
|
Changed: false,
|
|
Msg: msg,
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleFail(args map[string]any) (*TaskResult, error) {
|
|
msg := getStringArg(args, "msg", "Failed as requested")
|
|
return &TaskResult{
|
|
Failed: true,
|
|
Msg: msg,
|
|
}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleAssert(args map[string]any, host string) (*TaskResult, error) {
|
|
that, ok := args["that"]
|
|
if !ok {
|
|
return nil, coreerr.E("Executor.moduleAssert", "'that' required", nil)
|
|
}
|
|
|
|
conditions := normalizeConditions(that)
|
|
for _, cond := range conditions {
|
|
if !e.evalCondition(cond, host) {
|
|
msg := getStringArg(args, "fail_msg", sprintf("Assertion failed: %s", cond))
|
|
return &TaskResult{Failed: true, Msg: msg}, nil
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: false, Msg: "All assertions passed"}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleSetFact(args map[string]any) (*TaskResult, error) {
|
|
for k, v := range args {
|
|
if k != "cacheable" {
|
|
e.vars[k] = v
|
|
}
|
|
}
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) modulePause(ctx context.Context, args map[string]any) (*TaskResult, error) {
|
|
seconds := 0
|
|
if s, ok := args["seconds"].(int); ok {
|
|
seconds = s
|
|
}
|
|
if s, ok := args["seconds"].(string); ok {
|
|
seconds, _ = strconv.Atoi(s)
|
|
}
|
|
|
|
if seconds > 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-ctxSleep(ctx, seconds):
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: false}, nil
|
|
}
|
|
|
|
func ctxSleep(ctx context.Context, seconds int) <-chan struct{} {
|
|
ch := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-sleepChan(seconds):
|
|
}
|
|
close(ch)
|
|
}()
|
|
return ch
|
|
}
|
|
|
|
func sleepChan(seconds int) <-chan struct{} {
|
|
ch := make(chan struct{})
|
|
go func() {
|
|
for range seconds {
|
|
select {
|
|
case <-ch:
|
|
return
|
|
default:
|
|
// Sleep 1 second at a time
|
|
}
|
|
}
|
|
close(ch)
|
|
}()
|
|
return ch
|
|
}
|
|
|
|
func (e *Executor) moduleWaitFor(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
port := 0
|
|
if p, ok := args["port"].(int); ok {
|
|
port = p
|
|
}
|
|
host := getStringArg(args, "host", "127.0.0.1")
|
|
state := getStringArg(args, "state", "started")
|
|
timeout := 300
|
|
if t, ok := args["timeout"].(int); ok {
|
|
timeout = t
|
|
}
|
|
|
|
if port > 0 && state == "started" {
|
|
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 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: false}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleGit(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
repo := getStringArg(args, "repo", "")
|
|
dest := getStringArg(args, "dest", "")
|
|
version := getStringArg(args, "version", "HEAD")
|
|
|
|
if repo == "" || dest == "" {
|
|
return nil, coreerr.E("Executor.moduleGit", "repo and dest required", nil)
|
|
}
|
|
|
|
// Check if dest exists
|
|
exists, _ := client.FileExists(ctx, dest+"/.git")
|
|
|
|
var cmd string
|
|
if exists {
|
|
// Fetch and checkout (force to ensure clean state)
|
|
cmd = sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
|
|
} else {
|
|
cmd = sprintf("git clone %q %q && cd %q && git checkout %q",
|
|
repo, dest, dest, version)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleUnarchive(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
src := getStringArg(args, "src", "")
|
|
dest := getStringArg(args, "dest", "")
|
|
remote := getBoolArg(args, "remote_src", false)
|
|
|
|
if src == "" || dest == "" {
|
|
return nil, coreerr.E("Executor.moduleUnarchive", "src and dest required", nil)
|
|
}
|
|
|
|
// Create dest directory (best-effort)
|
|
_, _, _, _ = client.Run(ctx, sprintf("mkdir -p %q", dest))
|
|
|
|
var cmd string
|
|
if !remote {
|
|
// Upload local file first
|
|
data, err := localFS.Read(src)
|
|
if err != nil {
|
|
return nil, coreerr.E("Executor.moduleUnarchive", "read src", err)
|
|
}
|
|
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, sprintf("rm -f %q", tmpPath)) }()
|
|
}
|
|
|
|
// Detect archive type and extract
|
|
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 = sprintf("tar -xf %q -C %q", src, dest) // Guess tar
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func getStringArg(args map[string]any, key, def string) string {
|
|
if v, ok := args[key]; ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return sprintf("%v", v)
|
|
}
|
|
return def
|
|
}
|
|
|
|
func getBoolArg(args map[string]any, key string, def bool) bool {
|
|
if v, ok := args[key]; ok {
|
|
switch b := v.(type) {
|
|
case bool:
|
|
return b
|
|
case string:
|
|
lowered := lower(b)
|
|
return lowered == "true" || lowered == "yes" || lowered == "1"
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func normalizeGroups(v any) []string {
|
|
switch val := v.(type) {
|
|
case string:
|
|
if val == "" {
|
|
return nil
|
|
}
|
|
parts := split(val, ",")
|
|
groups := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if trimmed := corexTrimSpace(part); trimmed != "" {
|
|
groups = append(groups, trimmed)
|
|
}
|
|
}
|
|
return groups
|
|
case []string:
|
|
return val
|
|
case []any:
|
|
groups := make([]string, 0, len(val))
|
|
for _, item := range val {
|
|
if s, ok := item.(string); ok && corexTrimSpace(s) != "" {
|
|
groups = append(groups, corexTrimSpace(s))
|
|
}
|
|
}
|
|
return groups
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Additional Modules ---
|
|
|
|
func (e *Executor) moduleHostname(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleHostname", "name required", nil)
|
|
}
|
|
|
|
// Set hostname
|
|
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, sprintf("sed -i 's/127.0.1.1.*/127.0.1.1\t%s/' /etc/hosts", name))
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleSysctl(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
value := getStringArg(args, "value", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleSysctl", "name required", nil)
|
|
}
|
|
|
|
if state == "absent" {
|
|
// Remove from sysctl.conf
|
|
cmd := sprintf("sed -i '/%s/d' /etc/sysctl.conf", name)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// Set 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
|
|
}
|
|
|
|
// Persist if requested (best-effort)
|
|
if getBoolArg(args, "sysctl_set", true) {
|
|
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)
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleCron(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
job := getStringArg(args, "job", "")
|
|
state := getStringArg(args, "state", "present")
|
|
user := getStringArg(args, "user", "root")
|
|
|
|
minute := getStringArg(args, "minute", "*")
|
|
hour := getStringArg(args, "hour", "*")
|
|
day := getStringArg(args, "day", "*")
|
|
month := getStringArg(args, "month", "*")
|
|
weekday := getStringArg(args, "weekday", "*")
|
|
|
|
if state == "absent" {
|
|
if name != "" {
|
|
// Remove by name (comment marker)
|
|
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)
|
|
}
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// Build cron entry
|
|
schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
|
|
entry := sprintf("%s %s # %s", schedule, job, name)
|
|
|
|
// Add to crontab
|
|
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 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleBlockinfile(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
path := getStringArg(args, "path", "")
|
|
if path == "" {
|
|
path = getStringArg(args, "dest", "")
|
|
}
|
|
if path == "" {
|
|
return nil, coreerr.E("Executor.moduleBlockinfile", "path required", nil)
|
|
}
|
|
|
|
block := getStringArg(args, "block", "")
|
|
marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK")
|
|
state := getStringArg(args, "state", "present")
|
|
create := getBoolArg(args, "create", false)
|
|
|
|
beginMarker := replaceN(marker, "{mark}", "BEGIN", 1)
|
|
endMarker := replaceN(marker, "{mark}", "END", 1)
|
|
|
|
if state == "absent" {
|
|
// Remove block
|
|
cmd := sprintf("sed -i '/%s/,/%s/d' %q",
|
|
replaceAll(beginMarker, "/", "\\/"),
|
|
replaceAll(endMarker, "/", "\\/"),
|
|
path)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
// Create file if needed (best-effort)
|
|
if create {
|
|
_, _, _, _ = client.Run(ctx, sprintf("touch %q", path))
|
|
}
|
|
|
|
// Remove existing block and add new one
|
|
escapedBlock := replaceAll(block, "'", "'\\''")
|
|
cmd := sprintf(`
|
|
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
|
|
cat >> %q << 'BLOCK_EOF'
|
|
%s
|
|
%s
|
|
%s
|
|
BLOCK_EOF
|
|
`, replaceAll(beginMarker, "/", "\\/"),
|
|
replaceAll(endMarker, "/", "\\/"),
|
|
path, path, beginMarker, escapedBlock, endMarker)
|
|
|
|
stdout, stderr, rc, err := client.RunScript(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleIncludeVars(args map[string]any) (*TaskResult, error) {
|
|
file := getStringArg(args, "file", "")
|
|
if file == "" {
|
|
file = getStringArg(args, "_raw_params", "")
|
|
}
|
|
|
|
if file != "" {
|
|
// Would need to read and parse the vars file
|
|
// For now, just acknowledge
|
|
return &TaskResult{Changed: false, Msg: "include_vars: " + file}, nil
|
|
}
|
|
|
|
return &TaskResult{Changed: false}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleAddHost(args map[string]any) (*TaskResult, error) {
|
|
name := getStringArg(args, "name", "")
|
|
if name == "" {
|
|
name = getStringArg(args, "host", "")
|
|
}
|
|
if name == "" {
|
|
return nil, coreerr.E("Executor.moduleAddHost", "name required", nil)
|
|
}
|
|
|
|
if e.inventory == nil {
|
|
e.inventory = &Inventory{}
|
|
}
|
|
if e.inventory.All == nil {
|
|
e.inventory.All = &InventoryGroup{}
|
|
}
|
|
if e.inventory.All.Hosts == nil {
|
|
e.inventory.All.Hosts = make(map[string]*Host)
|
|
}
|
|
if e.inventory.All.Children == nil {
|
|
e.inventory.All.Children = make(map[string]*InventoryGroup)
|
|
}
|
|
|
|
host := e.inventory.All.Hosts[name]
|
|
if host == nil {
|
|
host = &Host{}
|
|
e.inventory.All.Hosts[name] = host
|
|
}
|
|
|
|
if v, ok := args["ansible_host"].(string); ok && v != "" {
|
|
host.AnsibleHost = v
|
|
}
|
|
if v, ok := args["ansible_port"].(int); ok && v > 0 {
|
|
host.AnsiblePort = v
|
|
}
|
|
if v, ok := args["ansible_user"].(string); ok && v != "" {
|
|
host.AnsibleUser = v
|
|
}
|
|
if v, ok := args["ansible_password"].(string); ok && v != "" {
|
|
host.AnsiblePassword = v
|
|
}
|
|
if v, ok := args["ansible_ssh_private_key_file"].(string); ok && v != "" {
|
|
host.AnsibleSSHPrivateKeyFile = v
|
|
}
|
|
if v, ok := args["ansible_connection"].(string); ok && v != "" {
|
|
host.AnsibleConnection = v
|
|
}
|
|
if v, ok := args["ansible_become_password"].(string); ok && v != "" {
|
|
host.AnsibleBecomePassword = v
|
|
}
|
|
|
|
if host.Vars == nil {
|
|
host.Vars = make(map[string]any)
|
|
}
|
|
if inlineVars, ok := args["vars"].(map[string]any); ok {
|
|
for k, v := range inlineVars {
|
|
host.Vars[k] = v
|
|
}
|
|
}
|
|
|
|
for _, groupName := range normalizeGroups(args["groups"]) {
|
|
if groupName == "" {
|
|
continue
|
|
}
|
|
group := e.inventory.All.Children[groupName]
|
|
if group == nil {
|
|
group = &InventoryGroup{}
|
|
e.inventory.All.Children[groupName] = group
|
|
}
|
|
if group.Hosts == nil {
|
|
group.Hosts = make(map[string]*Host)
|
|
}
|
|
group.Hosts[name] = host
|
|
}
|
|
|
|
return &TaskResult{Changed: true, Msg: "host added: " + name}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleMeta(args map[string]any) (*TaskResult, error) {
|
|
// meta module controls play execution
|
|
// Most actions are no-ops for us
|
|
return &TaskResult{Changed: false}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleSetup(ctx context.Context, client *SSHClient) (*TaskResult, error) {
|
|
// Gather facts - similar to what we do in gatherFacts
|
|
return &TaskResult{Changed: false, Msg: "facts gathered"}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleReboot(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
preRebootDelay := 0
|
|
if d, ok := args["pre_reboot_delay"].(int); ok {
|
|
preRebootDelay = d
|
|
}
|
|
|
|
msg := getStringArg(args, "msg", "Reboot initiated by Ansible")
|
|
|
|
if preRebootDelay > 0 {
|
|
cmd := sprintf("sleep %d && shutdown -r now '%s' &", preRebootDelay, msg)
|
|
_, _, _, _ = client.Run(ctx, cmd)
|
|
} else {
|
|
_, _, _, _ = client.Run(ctx, sprintf("shutdown -r now '%s' &", msg))
|
|
}
|
|
|
|
return &TaskResult{Changed: true, Msg: "Reboot initiated"}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleUFW(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
rule := getStringArg(args, "rule", "")
|
|
port := getStringArg(args, "port", "")
|
|
proto := getStringArg(args, "proto", "tcp")
|
|
state := getStringArg(args, "state", "")
|
|
|
|
var cmd string
|
|
|
|
// Handle state (enable/disable)
|
|
if state != "" {
|
|
switch state {
|
|
case "enabled":
|
|
cmd = "ufw --force enable"
|
|
case "disabled":
|
|
cmd = "ufw disable"
|
|
case "reloaded":
|
|
cmd = "ufw reload"
|
|
case "reset":
|
|
cmd = "ufw --force reset"
|
|
}
|
|
if cmd != "" {
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
}
|
|
|
|
// Handle rule
|
|
if rule != "" && port != "" {
|
|
switch rule {
|
|
case "allow":
|
|
cmd = sprintf("ufw allow %s/%s", port, proto)
|
|
case "deny":
|
|
cmd = sprintf("ufw deny %s/%s", port, proto)
|
|
case "reject":
|
|
cmd = sprintf("ufw reject %s/%s", port, proto)
|
|
case "limit":
|
|
cmd = sprintf("ufw limit %s/%s", port, proto)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
}
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleAuthorizedKey(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
user := getStringArg(args, "user", "")
|
|
key := getStringArg(args, "key", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if user == "" || key == "" {
|
|
return nil, coreerr.E("Executor.moduleAuthorizedKey", "user and key required", nil)
|
|
}
|
|
|
|
// Get user's home directory
|
|
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 := corexTrimSpace(stdout)
|
|
if home == "" {
|
|
home = "/root"
|
|
if user != "root" {
|
|
home = "/home/" + user
|
|
}
|
|
}
|
|
|
|
authKeysPath := joinPath(home, ".ssh", "authorized_keys")
|
|
|
|
if state == "absent" {
|
|
// Remove key
|
|
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, 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 := 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 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
// Fix permissions (best-effort)
|
|
_, _, _, _ = client.Run(ctx, sprintf("chmod 600 %q && chown %s:%s %q",
|
|
authKeysPath, user, user, authKeysPath))
|
|
|
|
return &TaskResult{Changed: true}, nil
|
|
}
|
|
|
|
func (e *Executor) moduleDockerCompose(ctx context.Context, client *SSHClient, args map[string]any) (*TaskResult, error) {
|
|
projectSrc := getStringArg(args, "project_src", "")
|
|
state := getStringArg(args, "state", "present")
|
|
|
|
if projectSrc == "" {
|
|
return nil, coreerr.E("Executor.moduleDockerCompose", "project_src required", nil)
|
|
}
|
|
|
|
var cmd string
|
|
switch state {
|
|
case "present":
|
|
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
|
|
case "absent":
|
|
cmd = sprintf("cd %q && docker compose down", projectSrc)
|
|
case "restarted":
|
|
cmd = sprintf("cd %q && docker compose restart", projectSrc)
|
|
default:
|
|
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
|
|
}
|
|
|
|
stdout, stderr, rc, err := client.Run(ctx, cmd)
|
|
if err != nil || rc != 0 {
|
|
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
|
}
|
|
|
|
// Heuristic for changed
|
|
changed := !contains(stdout, "Up to date") && !contains(stderr, "Up to date")
|
|
|
|
return &TaskResult{Changed: changed, Stdout: stdout}, nil
|
|
}
|