LoginSignup
6
3

More than 3 years have passed since last update.

Goexpectを使ってみた。(Golangによる対話式sshコマンド送信)

Last updated at Posted at 2019-05-16

Module I used

※こちらはLinux環境でしか使えないので、WSL等で実行してみてください。

How to install

$ go get github.com/google/goexpect
$ go get github.com/google/goterm/term

How to Use

今回はsshを使っていきたいと思います。

Googleさんが丁寧に載せてくださった例が見事に動かなかったので、sshとtelnetの箇所を参考にしながら以下のようにコードを書きました。

example.go
package main

import (
    "flag"
    "fmt"
    "log"
    "regexp"
    "time"

    "golang.org/x/crypto/ssh"

    "github.com/google/goexpect"
    "github.com/google/goterm/term"
)

const (
    timeout = 10 * time.Minute
)

var (
    addr  = flag.String("address", "", "address of telnet server")
    user  = flag.String("user", "user", "username to use")
    pass1 = flag.String("pass1", "pass1", "password to use")
    pass2 = flag.String("pass2", "pass2", "alternate password to use")
    cmd   = flag.String("cmd", "", "command to run")

    promptRE   = regexp.MustCompile("#")
    promptRE_2 = regexp.MustCompile(":")
)

func main() {
    flag.Parse()
    fmt.Println(term.Bluef("SSH Example"))

    sshClt, err := ssh.Dial("tcp", *addr, &ssh.ClientConfig{
        User:            *user,
        Auth:            []ssh.AuthMethod{ssh.Password(*pass1)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    })
    if err != nil {
        log.Fatalf("ssh.Dial(%q) failed: %v", *addr, err)
    }
    defer sshClt.Close()

    e, _, err := expect.SpawnSSH(sshClt, timeout)
    if err != nil {
        log.Fatal(err)
    }
    defer e.Close()

    e.Expect(promptRE, timeout)
    e.Send(*cmd + "\n")
    e.Expect(promptRE_2, timeout)
    e.Send("y" + "\n")
    result, _, _ := e.Expect(promptRE, timeout)
    e.Send("exit\n")

    fmt.Println(term.Greenf("%s: result: %s\n", *cmd, result))

    fmt.Println(term.Greenf("All done"))
}

解説

今回は、remote serverにて/root下に消す用のdirectoryである「for_rm」を作成しておきます。

以下のように実行をします。
(IP, Port, User, Passwordは実際使ったものとは異なります。)

$ go run practice.go -address 192.168.1.2:22 -user root -pass1 password -pass2 password -cmd "rm -r /root/for_rm"

すると、ここで入力したパラメータが

var (
   addr  = flag.String("address", "", "address of telnet server")
   user  = flag.String("user", "user", "username to use")
   pass1 = flag.String("pass1", "pass1", "password to use")
   pass2 = flag.String("pass2", "pass2", "alternate password to use")
   cmd   = flag.String("cmd", "", "command to run")
    )

こちらに入ります。

flag.Parse()

func main() {
    flag.Parse()

flag.Parse()をすることで、実行時のオプション指定の名前に対応する変数に対して、ポインタ指定をすると値を呼び出せるようになります。

ssh.Dial()

sshClt, err := ssh.Dial("tcp", *addr, &ssh.ClientConfig{
    User:            *user,
    Auth:            []ssh.AuthMethod{ssh.Password(*pass1)},
    HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
    log.Fatalf("ssh.Dial(%q) failed: %v", *addr, err)
}
defer sshClt.Close()

ここではおなじみの
"golang.org/x/crypto/ssh"
の使い方と同じようにDialしてあげます。

expect.SpawnSSH()

Spawn()を用いて、ptyを通してbashに対してcommandを投げてくれるインスタンスを生成します。

pty等に関しては、こちらにある内容がイメージしやすいのではないでしょうか。
http://eng-manima.blogspot.com/2014/09/pexpect.html

こちらの記事は、今回使っているGoexpectのpython版であるpexpectというものの解説です。

e, _, err := expect.SpawnSSH(sshClt, timeout)
if err != nil {
    log.Fatal(err)
}
defer e.Close()

ExpectとSend

上記のサイトでも説明がありますように、
Expect:bashから返ってくるもの
Send:実行するcommand

となっています。

/* 残りのvar
promptRE   = regexp.MustCompile("#")
promptRE_2 = regexp.MustCompile(":")
*/

e.Expect(promptRE, timeout)
e.Send(*cmd + "\n")
e.Expect(promptRE_2, timeout)
e.Send("y" + "\n")
result, _, _ := e.Expect(promptRE, timeout)
e.Send("exit\n")

Linuxでは、ログインして最初に帰ってくるものは ["user"@"hostname" ~]# のような形です。
e.Expect(promptRE, timeout)
とすることで、promptRE すなわち regexp.MustCompile("#")
つまり、# がついたものが返ってくることを明示的に示し(ubuntuの時は$に変えるなど、前もって決め打ちしなければなりません)

それに対して、e.Send(*cmd + "\n") によって、example.goの実行時に渡したcommandを送ります。
したがって

["user"@"hostname" ~]# rm -r /root/rm_fm

を実行することと同様のことをしています。

次では、実際のLinuxでは、「rm: ディレクトリ `for_rm/' を削除しますか? 」といったものが返ってくるので

promptRE_2 = regexp.MustCompile(":") によって、: を含むものが返ってくることを明示的に示し、
(例えば、「su -」を投げると、「Password: 」といったものが返ってくるので、: が無難だと思います。)

これに対して、e.Send("y" + "\n") をします。
すなわち

rm: ディレクトリ `for_rm/' を削除しますか? y

を実行することと同様のことを行われます。
こうして、無事対話式に対応して、directoryを消すことができました。

まとめ

windows対応版が欲しい...

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3