How do I execute a command on a remote machine in a golang CLI?

26,636

Solution 1

Try with os/exec https://golang.org/pkg/os/exec/ to execute a ssh

package main

import (
    "bytes"
    "log"
    "os/exec"
)

func main() {
    cmd := exec.Command("ssh", "remote-machine", "bash-command")
    var out bytes.Buffer
    cmd.Stdout = &out
    err := cmd.Run()
    if err != nil {
        log.Fatal(err)
    }
}

To jump over machines use the ProxyCommand directive in a ssh config file.

Host remote_machine_name
  ProxyCommand ssh -q bastion nc remote_machine_ip 22

Solution 2

You can run commands on a remote machine over SSH using the "golang.org/x/crypto/ssh" package.

Here is an example function demonstrating simple usage of running a single command on a remote machine and returning the output:

//e.g. output, err := remoteRun("root", "MY_IP", "PRIVATE_KEY", "ls")
func remoteRun(user string, addr string, privateKey string, cmd string) (string, error) {
    // privateKey could be read from a file, or retrieved from another storage
    // source, such as the Secret Service / GNOME Keyring
    key, err := ssh.ParsePrivateKey([]byte(privateKey))
    if err != nil {
        return "", err
    }
    // Authentication
    config := &ssh.ClientConfig{
        User: user,
        // https://github.com/golang/go/issues/19767 
        // as clientConfig is non-permissive by default 
        // you can set ssh.InsercureIgnoreHostKey to allow any host 
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(key),
        },
        //alternatively, you could use a password
        /*
            Auth: []ssh.AuthMethod{
                ssh.Password("PASSWORD"),
            },
        */
    }
    // Connect
    client, err := ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
    if err != nil {
        return "", err
    }
    // Create a session. It is one session per command.
    session, err := client.NewSession()
    if err != nil {
        return "", err
    }
    defer session.Close()
    var b bytes.Buffer  // import "bytes"
    session.Stdout = &b // get output
    // you can also pass what gets input to the stdin, allowing you to pipe
    // content from client to server
    //      session.Stdin = bytes.NewBufferString("My input")

    // Finally, run the command
    err = session.Run(cmd)
    return b.String(), err
}

Solution 3

The other solutions here will work, but I'll throw out another option you could try: simplessh. I think it is easier to use. For this question, I would use option 3 below where you can ssh using your key.

Option 1: SSH to a machine with a password, then run a command

import (
    "log"

    "github.com/sfreiberg/simplessh"
)

func main() error {
    var client *simplessh.Client
    var err error

    if client, err = simplessh.ConnectWithPassword("hostname_to_ssh_to", "username", "password"); err != nil {
        return err
    }

    defer client.Close()

    // Now run the commands on the remote machine:
    if _, err := client.Exec("cat /tmp/somefile"); err != nil {
        log.Println(err)
    }

    return nil
}

Option 2: SSH to a machine using a set of possible passwords, then run a command

import (
    "log"

    "github.com/sfreiberg/simplessh"
)

type access struct {
    login    string
    password string
}

var loginAccess []access

func init() {
    // Initialize all password to try
    loginAccess = append(loginAccess, access{"root", "rootpassword1"})
    loginAccess = append(loginAccess, access{"someuser", "newpassword"})
}

func main() error {
    var client *simplessh.Client
    var err error

    // Try to connect with first password, then tried second else fails gracefully
    for _, credentials := range loginAccess {
        if client, err = simplessh.ConnectWithPassword("hostname_to_ssh_to", credentials.login, credentials.password); err == nil {
            break
        }
    }

    if err != nil {
        return err
    }

    defer client.Close()

    // Now run the commands on the remote machine:
    if _, err := client.Exec("cat /tmp/somefile"); err != nil {
        log.Println(err)
    }

    return nil
}

Option 3: SSH to a machine using your key

import (
    "log"

    "github.com/sfreiberg/simplessh"
)

func SshAndRunCommand() error {
    var client *simplessh.Client
    var err error

    // Option A: Using a specific private key path:
    //if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to", "username", "/home/user/.ssh/id_rsa"); err != nil {

    // Option B: Using your default private key at $HOME/.ssh/id_rsa:
    //if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to", "username"); err != nil {

    // Option C: Use the current user to ssh and the default private key file:
    if client, err = simplessh.ConnectWithKeyFile("hostname_to_ssh_to"); err != nil {
        return err
    }

    defer client.Close()

    // Now run the commands on the remote machine:
    if _, err := client.Exec("cat /tmp/somefile"); err != nil {
        log.Println(err)
    }

    return nil
}

Solution 4

Try the package https://github.com/appleboy/easyssh-proxy

package main

import (
    "fmt"
    "time"

    "github.com/appleboy/easyssh-proxy"
)

func main() {
    // Create MakeConfig instance with remote username, server address and path to private key.
    ssh := &easyssh.MakeConfig{
        User:   "appleboy",
        Server: "example.com",
        // Optional key or Password without either we try to contact your agent SOCKET
        //Password: "password",
        // Paste your source content of private key
        // Key: `-----BEGIN RSA PRIVATE KEY-----
        // MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26
        // 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==
        // -----END RSA PRIVATE KEY-----
        // `,
        KeyPath: "/Users/username/.ssh/id_rsa",
        Port:    "22",
        Timeout: 60 * time.Second,
    }

    // Call Run method with command you want to run on remote server.
    stdout, stderr, done, err := ssh.Run("ls -al", 60*time.Second)
    // Handle errors
    if err != nil {
        panic("Can't run remote command: " + err.Error())
    } else {
        fmt.Println("don is :", done, "stdout is :", stdout, ";   stderr is :", stderr)
    }

}

See more example.

Solution 5

golang SSH executes shell command with timeout option

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "golang.org/x/crypto/ssh"
    "time"
)

func SshRemoteRunCommandWithTimeout(sshClient *ssh.Client, command string, timeout time.Duration) (string, error) {
    if timeout < 1 {
        return "", errors.New("timeout must be valid")
    }

    session, err := sshClient.NewSession()
    if err != nil {
        return "", err
    }
    defer session.Close()

    ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
    defer cancelFunc()
    resChan := make(chan string, 1)
    errChan := make(chan error, 1)

    go func() {
        // run shell script
        if output, err := session.CombinedOutput(command); err != nil {
            errChan <- err
        } else {
            resChan <- string(output)
        }
    }()

    select {
    case err := <-errChan:
        return "", err
    case ms := <-resChan:
        return ms, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
Share:
26,636
n8gard
Author by

n8gard

Updated on July 05, 2022

Comments

  • n8gard
    n8gard about 2 years

    How do I execute a command on a remote machine in a golang CLI? I need to write a golang CLI that can SSH into a remote machine via a key and execute a shell command. Furthermore, I need to be able to do this one hop away. e.g. SSH into a machine (like a cloud bastion) and then SSH into another, internal, machine and execute a shell command.

    I haven't (yet) found any examples for this.

  • JimB
    JimB about 8 years
    You can use the -W option in ssh rather than spawning an nc process
  • Chetan
    Chetan over 7 years
    What if the remote machine needs a password ? Is there a way to pass password along with this?
  • user2023507
    user2023507 about 7 years
    latest changes require hostkey checks. Issue resolution here - go-review.googlesource.com/c/38701
  • TheSporkboy
    TheSporkboy about 6 years
    The dangerous way to solve this it to add the InsecureIgnoreHostKey() supplied by the ssh library to the above: ``` config := &ssh.ClientConfig{ User: user, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Auth: []ssh.AuthMethod{ <snip> ``` The proper way would be to add them to your allows_hosts file and validate against that. Hopefully someone else can add that example.
  • Katie
    Katie almost 6 years
    How can I make the output be in utf8 form? I just see a bunch of digits :/
  • Ricardo Silva
    Ricardo Silva almost 5 years
    Every time I run this piece of code, it returns the following message: ssh: no key found. Any ideas on fixing it? because the key exists and its valid. I use it to connect to any other server.
  • colm.anseo
    colm.anseo about 4 years
    @Katie are you trying to render the bytes.Buffer value? bytes will render as lists of int8 values if you use printf's %v directive. To render as a printable string ensure to use %s.
  • Eric Zhou
    Eric Zhou about 3 years
    Is there any way to add a timeout parameter of ssh-remote-shell-command execution in the function?