Files
llmgw/internal/onboard/ssh.go
T
Ilker ManapandClaude Opus 5.5 0d78f9bb34 A machine given by address is reached through its ssh config Host
node add (and server add, mirror peer, patroni init) is usually given an
address, while the ssh config names the machine: `Host gpu01` with
`HostName 10.0.0.21` and the User, IdentityFile or ProxyJump it needs. That
block did not match the address, so the address had to be added to its
Host line by hand. Now the concrete Host names in the config and its
Includes are read, ssh -G says each one's HostName, and the first that is
the address (or resolves to it) is connected through.

Under sudo, the caller's name is now a fallback: a User in the config's
block for the machine comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 10:42:23 +01:00

94 lines
3.0 KiB
Go

package onboard
import (
"bytes"
"context"
"os/exec"
"runtime"
"sync"
)
// Remote runs a shell command on the machine being onboarded, with input.
type Remote interface {
Run(ctx context.Context, script string, stdin []byte) ([]byte, error)
}
// SSH is Remote through the system's ssh, so the operator's keys, agent and
// ~/.ssh/config apply exactly as they do when they log in by hand.
//
// Addr is looked up in the ssh config first: a Host block whose HostName is
// Addr is connected through by its name, so its User, IdentityFile and
// ProxyJump apply to a machine given by address, without the address
// having to be added to its Host line.
type SSH struct {
Addr, User string
// FallbackUser logs in when User is empty and the ssh config names no
// user for the machine: under sudo, the person who ran it.
FallbackUser string
once sync.Once
target string
}
// Target is what ssh is given: [user@]host, host being the ssh config's
// name for Addr when it has one.
func (s *SSH) Target(ctx context.Context) string {
s.once.Do(func() {
host := s.Addr
if a := configAlias(ctx, s.Addr); a != "" {
host = a
}
u := s.User
if u == "" && configUser(ctx, host) == "" {
u = s.FallbackUser
}
s.target = host
if u != "" {
s.target = u + "@" + host
}
})
return s.target
}
// sshOptions never prompt: a question nobody is there to answer would hang
// onboarding instead of failing it.
//
// accept-new rather than refusing unknown host keys: a machine provisioned a
// minute ago has a key nobody has seen, and the risk guarded against is a
// substituted machine, whose changed key is still refused.
var sshOptions = []string{
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=10",
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ServerAliveInterval=15",
// "Permanently added … to the list of known hosts" and the like are
// for a person at a prompt; here they would be read as the answer.
"-o", "LogLevel=ERROR",
}
// Run runs script with sh on the machine. Files travel on standard input,
// not scp, which a hardened sshd may not offer.
func (s *SSH) Run(ctx context.Context, script string, stdin []byte) ([]byte, error) {
target := s.Target(ctx)
// "--" before the target: an address beginning with "-" is not read as
// an option.
args := append(append([]string{}, sshOptions...), "--", target, "sh", "-c", shellQuote(script))
cmd := exec.CommandContext(ctx, "ssh", args...)
if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
// The answer is standard output alone: ssh and the machine's login
// scripts write to standard error, and mixed in they were taken for the
// answer — the first connection's "Warning: Permanently added" read as
// the machine's hostname. Standard error comes back only with a failure,
// where it is usually the reason.
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
return append(stdout.Bytes(), stderr.Bytes()...), err
}
return stdout.Bytes(), nil
}
func hostArch() string { return runtime.GOARCH }