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

# Module I used
https://github.com/google/goexpect

※こちらは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対応版が欲しい...