1
0
mirror of https://github.com/rancher/cli.git synced 2026-02-05 09:48:36 +01:00
Files
cli/cmd/ssh.go
Donnie Adams 1a2f5d2bb5 Support SSH for v2 provisioned machines
Before this change, it was not possible to use the SSH command to
connect to machines provisioned with v2 provisioning. After this change
(and including the changes to Rancher), the CLI will use the new CAPI
client to get the SSH key and config from Rancher for v2 provisioned
machines.

A side effect of this change is the addition of the new `rancher
machines ls` command that lists all machines for the current cluster
context.
2021-11-01 17:24:12 -07:00

245 lines
5.3 KiB
Go

package cmd
import (
"archive/zip"
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"strings"
"github.com/pkg/errors"
"github.com/rancher/cli/cliclient"
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
"github.com/urfave/cli"
)
const sshDescription = `
For any nodes created through Rancher using docker-machine,
you can SSH into the node. This is not supported for any custom nodes.
Examples:
# SSH into a node by ID/name
$ rancher ssh nodeFoo
# SSH into a node by ID/name using the external IP address
$ rancher ssh -e nodeFoo
# SSH into a node by name but specify the login name to use
$ rancher ssh -l login1 nodeFoo
# SSH into a node by specifying login name and node using the @ syntax while adding a command to run
$ rancher ssh login1@nodeFoo -- netstat -p tcp
`
func SSHCommand() cli.Command {
return cli.Command{
Name: "ssh",
Usage: "SSH into a node",
Description: sshDescription,
Action: nodeSSH,
ArgsUsage: "[NODE_ID/NODE_NAME]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "external,e",
Usage: "Use the external ip address of the node",
},
cli.StringFlag{
Name: "login,l",
Usage: "The login name",
},
},
}
}
func nodeSSH(ctx *cli.Context) error {
args := ctx.Args()
if len(args) > 0 && (args[0] == "-h" || args[0] == "--help") {
return cli.ShowCommandHelp(ctx, "ssh")
}
if ctx.NArg() == 0 {
return cli.ShowCommandHelp(ctx, "ssh")
}
user := ctx.String("login")
nodeName := ctx.Args().First()
if strings.Contains(nodeName, "@") {
user = strings.Split(nodeName, "@")[0]
nodeName = strings.Split(nodeName, "@")[1]
}
args = args[1:]
c, err := GetClient(ctx)
if err != nil {
return err
}
sshNode, key, err := getNodeAndKey(ctx, c, nodeName)
if err != nil {
return err
}
if user == "" {
user = sshNode.SshUser
}
ipAddress := sshNode.IPAddress
if ctx.Bool("external") {
ipAddress = sshNode.ExternalIPAddress
}
return processExitCode(callSSH(key, ipAddress, user, args))
}
func getNodeAndKey(ctx *cli.Context, c *cliclient.MasterClient, nodeName string) (managementClient.Node, []byte, error) {
sshNode := managementClient.Node{}
resource, err := Lookup(c, nodeName, "node")
if err != nil {
return sshNode, nil, err
}
sshNode, err = getNodeByID(ctx, c, resource.ID)
if err != nil {
return sshNode, nil, err
}
link := sshNode.Links["nodeConfig"]
if link == "" {
// Get the machine and use that instead.
machine, err := getMachineByNodeName(ctx, c, nodeName)
if err != nil {
return sshNode, nil, fmt.Errorf("failed to find SSH key for node [%s]", nodeName)
}
link = machine.Links["sshkeys"]
}
key, sshUser, err := getSSHKey(c, link, getNodeName(sshNode))
if err != nil {
return sshNode, nil, err
}
if sshUser != "" {
sshNode.SshUser = sshUser
}
return sshNode, key, nil
}
func callSSH(content []byte, ip string, user string, args []string) error {
dest := fmt.Sprintf("%s@%s", user, ip)
tmpfile, err := ioutil.TempFile("", "ssh")
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if err := os.Chmod(tmpfile.Name(), 0600); err != nil {
return err
}
_, err = tmpfile.Write(content)
if err != nil {
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
cmd := exec.Command("ssh", append([]string{"-i", tmpfile.Name(), dest}, args...)...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
}
func getSSHKey(c *cliclient.MasterClient, link, nodeName string) ([]byte, string, error) {
if link == "" {
return nil, "", fmt.Errorf("failed to find SSH key for %s", nodeName)
}
req, err := http.NewRequest("GET", link, nil)
if err != nil {
return nil, "", err
}
req.SetBasicAuth(c.UserConfig.AccessKey, c.UserConfig.SecretKey)
req.Header.Add("Accept-Encoding", "zip")
client := &http.Client{}
if c.UserConfig.CACerts != "" {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(c.UserConfig.CACerts))
if !ok {
return []byte{}, "", err
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: roots,
},
}
client.Transport = tr
}
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
zipFiles, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
if resp.StatusCode != 200 {
return nil, "", fmt.Errorf("%s", zipFiles)
}
zipReader, err := zip.NewReader(bytes.NewReader(zipFiles), resp.ContentLength)
if err != nil {
return nil, "", err
}
var sshKey []byte
var sshUser string
for _, file := range zipReader.File {
if path.Base(file.Name) == "id_rsa" {
sshKey, err = readFile(file)
if err != nil {
return nil, "", err
}
} else if path.Base(file.Name) == "config.json" {
config, err := readFile(file)
if err != nil {
return nil, "", err
}
var data map[string]interface{}
err = json.Unmarshal(config, &data)
if err != nil {
return nil, "", err
}
sshUser, _ = data["SSHUser"].(string)
}
}
if len(sshKey) == 0 {
return sshKey, "", errors.New("can't find private key file")
}
return sshKey, sshUser, nil
}
func readFile(file *zip.File) ([]byte, error) {
r, err := file.Open()
if err != nil {
return nil, err
}
defer r.Close()
return ioutil.ReadAll(r)
}