E1: split monolithic files — cmd/nomos (main.go → server.go + mcp.go + workers.go),
internal/mcp/tools.go → entity_tools/ops_tools/knowledge_tools/analysis_tools,
internal/httpapi/impl.go → domain files (entities, events, signals, ontology,
fleet_health, client_context, client_lifecycle, entity_mutations, query_audit).
E2: migrate raw pool.Exec queries to sqlc (entities/relationships queries + generated).
E3: unify SSH — consolidate crypto/ssh dial into actuator/client.go (+client_test).
E4/E5: add tests — db/lifecycle, checkdefaults/build, ontology/preconditions, policy/risk.
102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
package actuator
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// defaultDialTimeout bounds an SSH dial when the caller leaves Timeout unset.
|
|
// 10s matches the previous hardcoded value at every dial site.
|
|
const defaultDialTimeout = 10 * time.Second
|
|
|
|
// LoadSigner reads and parses the private key at keyPath.
|
|
func LoadSigner(keyPath string) (ssh.Signer, error) {
|
|
key, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read ssh key: %w", err)
|
|
}
|
|
return LoadSignerFromBytes(key)
|
|
}
|
|
|
|
// LoadSignerFromBytes parses an in-memory private key into an ssh.Signer.
|
|
func LoadSignerFromBytes(key []byte) (ssh.Signer, error) {
|
|
signer, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse ssh key: %w", err)
|
|
}
|
|
return signer, nil
|
|
}
|
|
|
|
// DialOptions configures an SSH dial.
|
|
type DialOptions struct {
|
|
Host string
|
|
Port int // 0 means 22
|
|
User string
|
|
Signer ssh.Signer
|
|
Timeout time.Duration // dial timeout; <=0 means defaultDialTimeout
|
|
}
|
|
|
|
// Dial opens a crypto/ssh connection through the centralized HostKeyCallback.
|
|
// The connection itself is bounded by Timeout; ctx is respected by callers
|
|
// via RunCombinedOutput once the session is running.
|
|
func Dial(ctx context.Context, opts DialOptions) (*ssh.Client, error) {
|
|
port := opts.Port
|
|
if port <= 0 {
|
|
port = 22
|
|
}
|
|
timeout := opts.Timeout
|
|
if timeout <= 0 {
|
|
timeout = defaultDialTimeout
|
|
}
|
|
cfg := &ssh.ClientConfig{
|
|
User: opts.User,
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(opts.Signer)},
|
|
HostKeyCallback: HostKeyCallback(),
|
|
Timeout: timeout,
|
|
}
|
|
addr := net.JoinHostPort(opts.Host, fmt.Sprintf("%d", port))
|
|
client, err := ssh.Dial("tcp", addr, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ssh dial %s:%d: %w", opts.Host, port, err)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// RunCombinedOutput runs cmd on an established client and returns its combined
|
|
// stdout/stderr. Context cancellation closes the session to abort the remote
|
|
// command instead of blocking until it finishes — the same goroutine+select
|
|
// pattern the actuator, mcp, and scheduler each reimplemented before.
|
|
func RunCombinedOutput(ctx context.Context, client *ssh.Client, cmd string) ([]byte, error) {
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create session: %w", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
type result struct {
|
|
out []byte
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
go func() {
|
|
out, err := session.CombinedOutput(cmd)
|
|
ch <- result{out: out, err: err}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
session.Close()
|
|
return nil, ctx.Err()
|
|
case res := <-ch:
|
|
if res.err != nil {
|
|
return res.out, fmt.Errorf("command: %w", res.err)
|
|
}
|
|
return res.out, nil
|
|
}
|
|
}
|