Files

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 }