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 }