package probes import ( "context" "fmt" "strings" "github.com/dtoro/oikos/internal/actuator" "github.com/dtoro/oikos/internal/core/ports" "golang.org/x/crypto/ssh" ) // SSHChecker runs ssh-script probes via the actuator. Constructed with a // key source so the caller controls SSH key resolution. type SSHChecker struct { pool *actuator.DialPool signerFn func() (ssh.Signer, error) } func NewSSHChecker(pool *actuator.DialPool, signerFn func() (ssh.Signer, error)) *SSHChecker { return &SSHChecker{pool: pool, signerFn: signerFn} } func (c *SSHChecker) Check(ctx context.Context, def ports.CheckDef, target ports.Target) ports.CheckResult { cfg := configMap(def) script, _ := parseStr(cfg, "script") if script == "" { return ports.CheckResult{State: "unknown", Message: "no script in config"} } signer, err := c.signerFn() if err != nil { return ports.CheckResult{State: "unknown", Message: fmt.Sprintf("signer: %v", err)} } client, err := actuator.Dial(ctx, actuator.DialOptions{ Host: target.Host, User: target.User, Signer: signer, }) if err != nil { return ports.CheckResult{State: "unknown", Message: fmt.Sprintf("dial %s: %v", target.Host, err)} } defer client.Close() cmd := fmt.Sprintf("/opt/oikos/checks/%s", script) if target.Wrap != nil { cmd = target.Wrap(cmd) } output, err := actuator.RunCombinedOutput(ctx, client, cmd) if err != nil { return ports.CheckResult{ Value: -1, State: "critical", Message: err.Error(), } } return parseSSHResult(output) } func parseSSHResult(output []byte) ports.CheckResult { line := strings.TrimSpace(string(output)) var value float64 if len(line) > 0 { parts := strings.SplitN(line, " ", 3) switch parts[0] { case "OK": if len(parts) > 1 { fmt.Sscanf(parts[1], "%f", &value) } return ports.CheckResult{State: "ok", Value: value, Message: line} case "WARN": if len(parts) > 1 { fmt.Sscanf(parts[1], "%f", &value) } return ports.CheckResult{State: "warning", Value: value, Message: line} case "CRIT": if len(parts) > 1 { fmt.Sscanf(parts[1], "%f", &value) } return ports.CheckResult{State: "critical", Value: value, Message: line} } } return ports.CheckResult{State: "unknown", Message: "unparseable output"} } // Registry maps check kinds to Checker implementations. type Registry map[string]ports.Checker func NewRegistry() Registry { return Registry{ "http": NewHTTPChecker(), "tcp": NewTCPChecker(), "ping": NewPingChecker(), "dns": NewDNSChecker(), "vm-status": nil, "ssh-script": nil, "backup-freshness": nil, "cert-expiry": nil, } } func (r Registry) Get(kind string) ports.Checker { return r[kind] } func (r Registry) Register(kind string, c ports.Checker) { r[kind] = c }