Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
Go
65 lines
2.2 KiB
Go
package onboard
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os/exec"
|
|
"runtime"
|
|
)
|
|
|
|
// 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.
|
|
type SSH struct {
|
|
Addr, User string
|
|
}
|
|
|
|
// 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.Addr
|
|
if s.User != "" {
|
|
target = s.User + "@" + s.Addr
|
|
}
|
|
// "--" 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 }
|