X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/56e130608f8977d20b21c54f6ab8973d71e045a0..37d9f94b06ff367a3514b58ec6f0e4d4d0116030:/cmd/arvados-client/container_gateway.go diff --git a/cmd/arvados-client/container_gateway.go b/cmd/arvados-client/container_gateway.go index 46fa6932b3..aca6c5b797 100644 --- a/cmd/arvados-client/container_gateway.go +++ b/cmd/arvados-client/container_gateway.go @@ -5,6 +5,7 @@ package main import ( + "bytes" "context" "flag" "fmt" @@ -12,9 +13,11 @@ import ( "net/url" "os" "os/exec" + "path/filepath" "strings" "syscall" + "git.arvados.org/arvados.git/lib/cmd" "git.arvados.org/arvados.git/lib/controller/rpc" "git.arvados.org/arvados.git/sdk/go/arvados" ) @@ -25,57 +28,64 @@ type shellCommand struct{} func (shellCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { f := flag.NewFlagSet(prog, flag.ContinueOnError) - f.SetOutput(stderr) - f.Usage = func() { - fmt.Print(stderr, prog+`: open an interactive shell on a running container. - -Usage: `+prog+` [options] container-uuid [ssh-options] [remote-command [args...]] - -Options: -`) - f.PrintDefaults() - } detachKeys := f.String("detach-keys", "ctrl-],ctrl-]", "set detach key sequence, as in docker-attach(1)") - err := f.Parse(args) - if err != nil { - fmt.Println(stderr, err) - f.Usage() + if ok, code := cmd.ParseFlags(f, prog, args, "[username@]container-uuid [ssh-options] [remote-command [args...]]", stderr); !ok { + return code + } else if f.NArg() < 1 { + fmt.Fprintf(stderr, "missing required argument: container-uuid (try -help)\n") return 2 } - - if f.NArg() < 1 { - f.Usage() - return 2 + target := f.Args()[0] + if !strings.Contains(target, "@") { + target = "root@" + target } - targetUUID := f.Args()[0] sshargs := f.Args()[1:] + // Try setting up a tunnel, and exit right away if it + // fails. This tunnel won't get used -- we'll set up a new + // tunnel when running as SSH client's ProxyCommand child -- + // but in most cases where the real tunnel setup would fail, + // we catch the problem earlier here. This makes it less + // likely that an error message about tunnel setup will get + // hidden behind noisy errors from SSH client like this: + // + // [useful tunnel setup error message here] + // kex_exchange_identification: Connection closed by remote host + // Connection closed by UNKNOWN port 65535 + // exit status 255 + // + // In case our target is a container request, the probe also + // resolves it to a container, so we don't connect to two + // different containers in a race. + var probetarget bytes.Buffer + exitcode := connectSSHCommand{}.RunCommand( + "arvados-client connect-ssh", + []string{"-detach-keys=" + *detachKeys, "-probe-only=true", target}, + &bytes.Buffer{}, &probetarget, stderr) + if exitcode != 0 { + return exitcode + } + target = strings.Trim(probetarget.String(), "\n") + selfbin, err := os.Readlink("/proc/self/exe") if err != nil { fmt.Fprintln(stderr, err) return 2 } sshargs = append([]string{ - "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys='" + strings.Replace(*detachKeys, "'", "'\\''", -1) + "' " + targetUUID, + "ssh", + "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target), "-o", "StrictHostKeyChecking no", - "root@" + targetUUID}, + target}, sshargs...) - cmd := exec.Command("ssh", sshargs...) - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - err = cmd.Run() - if err == nil { - return 0 - } else if exiterr, ok := err.(*exec.ExitError); !ok { - fmt.Fprintln(stderr, err) - return 1 - } else if status, ok := exiterr.Sys().(syscall.WaitStatus); !ok { + sshbin, err := exec.LookPath("ssh") + if err != nil { fmt.Fprintln(stderr, err) return 1 - } else { - return status.ExitStatus() } + err = syscall.Exec(sshbin, sshargs, os.Environ()) + fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err) + return 1 } // connectSSHCommand connects stdin/stdout to a container's gateway @@ -89,24 +99,36 @@ func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader, f := flag.NewFlagSet(prog, flag.ContinueOnError) f.SetOutput(stderr) f.Usage = func() { + _, prog := filepath.Split(prog) fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container. -Usage: `+prog+` [options] container-uuid +NOTE: You almost certainly don't want to use this command directly. It +is meant to be used internally. Use "arvados-client shell" instead. + +Usage: `+prog+` [options] [username@]container-uuid Options: `) f.PrintDefaults() } + probeOnly := f.Bool("probe-only", false, "do not transfer IO, just setup tunnel, print target UUID, and exit") detachKeys := f.String("detach-keys", "", "set detach key sequence, as in docker-attach(1)") - if err := f.Parse(args); err != nil { - fmt.Fprintln(stderr, err) - f.Usage() - return 2 + if ok, code := cmd.ParseFlags(f, prog, args, "[username@]container-uuid", stderr); !ok { + return code } else if f.NArg() != 1 { - f.Usage() + fmt.Fprintf(stderr, "missing required argument: [username@]container-uuid\n") return 2 } targetUUID := f.Args()[0] + loginUsername := "root" + if i := strings.Index(targetUUID, "@"); i >= 0 { + loginUsername = targetUUID[:i] + targetUUID = targetUUID[i+1:] + } + if os.Getenv("ARVADOS_API_HOST") == "" || os.Getenv("ARVADOS_API_TOKEN") == "" { + fmt.Fprintln(stderr, "fatal: ARVADOS_API_HOST and ARVADOS_API_TOKEN environment variables are not set") + return 1 + } insecure := os.Getenv("ARVADOS_API_HOST_INSECURE") rpcconn := rpc.NewConn("", &url.URL{ @@ -117,28 +139,43 @@ Options: func(context.Context) ([]string, error) { return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil }) - // if strings.Contains(targetUUID, "-xvhdp-") { - // cr, err := rpcconn.ContainerRequestGet(context.TODO(), arvados.GetOptions{UUID: targetUUID}) - // if err != nil { - // fmt.Fprintln(stderr, err) - // return 1 - // } - // if cr.ContainerUUID == "" { - // fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(cr.State)) - // return 1 - // } - // targetUUID = cr.ContainerUUID - // } + if strings.Contains(targetUUID, "-xvhdp-") { + crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}}) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + if len(crs.Items) < 1 { + fmt.Fprintf(stderr, "container request %q not found\n", targetUUID) + return 1 + } + cr := crs.Items[0] + if cr.ContainerUUID == "" { + fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State))) + return 1 + } + targetUUID = cr.ContainerUUID + fmt.Fprintln(stderr, "connecting to container", targetUUID) + } else if !strings.Contains(targetUUID, "-dz642-") { + fmt.Fprintf(stderr, "target UUID is not a container or container request UUID: %s\n", targetUUID) + return 1 + } sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{ - UUID: targetUUID, - DetachKeys: *detachKeys, + UUID: targetUUID, + DetachKeys: *detachKeys, + LoginUsername: loginUsername, }) if err != nil { - fmt.Fprintln(stderr, err) + fmt.Fprintln(stderr, "error setting up tunnel:", err) return 1 } defer sshconn.Conn.Close() + if *probeOnly { + fmt.Fprintln(stdout, targetUUID) + return 0 + } + ctx, cancel := context.WithCancel(context.Background()) go func() { defer cancel() @@ -157,3 +194,7 @@ Options: <-ctx.Done() return 0 } + +func shellescape(s string) string { + return "'" + strings.Replace(s, "'", "'\\''", -1) + "'" +}