cmd/gomote: implements GRPC ssh command
This change adds the implementation for the GRPC ssh command to the
gomote client.
Updates golang/go#48737
For golang/go#47521
Change-Id: I1e1f09ad23b0f07d28e0c5d06ad00cb948bb41f8
Reviewed-on: https://go-review.googlesource.com/c/build/+/405514
Reviewed-by: Alex Rakoczy <alex@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Carlos Amedee <carlos@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index 1d18cda..4df0288 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -162,7 +162,7 @@
registerCommand("rdp", "RDP (Remote Desktop Protocol) to a Windows buildlet", rdp)
registerCommand("rm", "delete files or directories", rm)
registerCommand("run", "run a command on a buildlet", legacyRun)
- registerCommand("ssh", "ssh to a buildlet", ssh)
+ registerCommand("ssh", "ssh to a buildlet", legacySSH)
registerCommand("v2", "version 2 of the gomote commands", version2)
}
@@ -222,6 +222,7 @@
"ls": ls,
"run": run,
"ping": ping,
+ "ssh": ssh,
}
if len(args) == 0 {
usage()
diff --git a/cmd/gomote/ssh.go b/cmd/gomote/ssh.go
index b870013..21f9f11 100644
--- a/cmd/gomote/ssh.go
+++ b/cmd/gomote/ssh.go
@@ -5,18 +5,23 @@
package main
import (
+ "context"
+ "errors"
"flag"
"fmt"
+ "io/ioutil"
"log"
"os"
"os/exec"
+ "path/filepath"
"strings"
"syscall"
+ "golang.org/x/build/internal/gomote/protos"
"golang.org/x/build/internal/gophers"
)
-func ssh(args []string) error {
+func legacySSH(args []string) error {
fs := flag.NewFlagSet("ssh", flag.ContinueOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "ssh usage: gomote ssh <instance>")
@@ -53,3 +58,119 @@
syscall.Exec(ssh, []string{"ssh", "-p", "2222", sshUser + "@farmer.golang.org"}, os.Environ())
return nil
}
+
+func ssh(args []string) error {
+ fs := flag.NewFlagSet("ssh", flag.ContinueOnError)
+ fs.Usage = func() {
+ fmt.Fprintln(os.Stderr, "ssh usage: gomote ssh <instance>")
+ fs.PrintDefaults()
+ os.Exit(1)
+ }
+ fs.Parse(args)
+ if fs.NArg() != 1 {
+ fs.Usage()
+ }
+
+ name := fs.Arg(0)
+ sshKeyDir, err := sshConfigDirectory()
+ if err != nil {
+ return err
+ }
+ pubKey, priKey, err := localKeyPair(sshKeyDir)
+ if err != nil {
+ return err
+ }
+ pubKeyBytes, err := os.ReadFile(pubKey)
+ if err != nil {
+ return err
+ }
+ ctx := context.Background()
+ client := gomoteServerClient(ctx)
+ resp, err := client.SignSSHKey(ctx, &protos.SignSSHKeyRequest{
+ GomoteId: name,
+ PublicSshKey: []byte(pubKeyBytes),
+ })
+ if err != nil {
+ return fmt.Errorf("unable to retrieve SSH certificate: %s", statusFromError(err))
+ }
+ certPath, err := writeCertificateToDisk(resp.GetSignedPublicSshKey())
+ if err != nil {
+ return err
+ }
+ return sshConnect(name, priKey, certPath)
+}
+
+func sshConfigDirectory() (string, error) {
+ configDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", fmt.Errorf("unable to retrieve user configuration directory: %s", err)
+ }
+ sshConfigDir := filepath.Join(configDir, "gomote", ".ssh")
+ err = os.MkdirAll(sshConfigDir, 0700)
+ if err != nil {
+ return "", fmt.Errorf("unable to create user SSH configuration directory: %s", err)
+ }
+ return sshConfigDir, nil
+}
+
+func localKeyPair(sshDir string) (string, string, error) {
+ priKey := filepath.Join(sshDir, "id_ed25519")
+ pubKey := filepath.Join(sshDir, "id_ed25519.pub")
+ if !fileExists(priKey) || !fileExists(pubKey) {
+ log.Printf("local ssh keys do not exist, attempting to create them")
+ if err := createLocalKeyPair(pubKey, priKey); err != nil {
+ return "", "", fmt.Errorf("unable to create local SSH key pair: %s", err)
+ }
+ }
+ return pubKey, priKey, nil
+}
+
+func createLocalKeyPair(pubKey, priKey string) error {
+ cmd := exec.Command("ssh-keygen", "-o", "-a", "256", "-t", "ed25519", "-f", priKey)
+ cmd.Stdout = os.Stdout
+ cmd.Stdin = os.Stdin
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func writeCertificateToDisk(b []byte) (string, error) {
+ tmpDir := filepath.Join(os.TempDir(), ".gomote")
+ if err := os.MkdirAll(tmpDir, 0700); err != nil {
+ return "", fmt.Errorf("unable to create temp directory for certficates: %s", err)
+ }
+ tf, err := ioutil.TempFile(tmpDir, "id_ed25519-*-cert.pub")
+ if err != nil {
+ return "", err
+ }
+ if err := tf.Chmod(0600); err != nil {
+ return "", err
+ }
+ if _, err := tf.Write(b); err != nil {
+ return "", err
+ }
+ return tf.Name(), tf.Close()
+}
+
+func sshConnect(name string, priKey, certPath string) error {
+ ssh, err := exec.LookPath("ssh")
+ if err != nil {
+ return fmt.Errorf("path to ssh not found: %s", err)
+ }
+ cli := []string{"-o", fmt.Sprintf("CertificateFile=%s", certPath), "-i", priKey, "-p", "2222", name + "@farmer.golang.org"}
+ fmt.Printf("$ %s %s\n", ssh, strings.Join(cli, " "))
+ cmd := exec.Command(ssh, cli...)
+ cmd.Stdout = os.Stdout
+ cmd.Stdin = os.Stdin
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("unable to ssh into instance: %s", err)
+ }
+ return nil
+}
+
+func fileExists(path string) bool {
+ if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
+ return false
+ }
+ return true
+}