Golang 自作sshコマンド送信ツールを使ってログ回収

# 本題に入る前に

私は、会社でDjangoを用いた社内ツールを作りました。
内容としては、ある機器をConfigurationする際の設計資料とログファイルを用いた作業結果の確認ツールです。

設計資料はExcelで作られているため、SQLiteのtableに入れやすかったのですが、
ログファイルをその設計資料と全く同じtableの形にするのは少し大変でしたね...(早くGoの話しろよ)

そんなこんなで、ツールをデプロイしたのですが、毎度ある項目に関しては情報を引き出すために5つのcommandを打たなくてはならなくて、不便だなって思ったので今回のGoによるツールを作ろうと思ったわけです。

# Code

以下がコードになります。
所々(func1等)コードを書き換えていますので、流れを把握していただければと思います。

```logCollect.go
package main

import (
	"flag"
	"strings"
	"sync"
	"time"

	"github.com/ryojsb/paramigo"
)

var (
	host     string
	port     string
	user     string
	password string
	id       string
	path1    string
	path2    string
)

const (
	format = "2000-01-01"
)

func Func1(date string, id string, path1 string, path2 string, wg *sync.WaitGroup) {
	cmd := "BinPATH{Log出力command} > FilePATHDATE_ID_<file名>.txt"
	cmd = strings.ReplaceAll(cmd, "BinPATH", path1)
	cmd = strings.ReplaceAll(cmd, "FilePATH", path2)
	cmd = strings.Replace(cmd, "DATE", date, 1)
	cmd = strings.ReplaceAll(cmd, "ID", id)
	paramigo.InnerCommand(host, port, user, password, cmd)
	wg.Done()
}

<他4つ 省略>

func main() {
	flag.StringVar(&host, "host", "", "IP Address")
	flag.StringVar(&port, "port", "", "Port")
	flag.StringVar(&user, "u", "", "user")
	flag.StringVar(&password, "p", "", "Password")
	flag.StringVar(&id, "id", "", "S/N")
	flag.StringVar(&path1, "path1", "", "PATH for bin")
	flag.StringVar(&path2, "path2", "", "PATH for log file")
	flag.Parse()

	t := time.Now()
	formattedTime := t.Format(format)
	date := strings.ReplaceAll(formattedTime, "-", "")

	var wg sync.WaitGroup
	wg.Add(5)
	go func1(date, id, path1, path2, &wg)
	go func2(date, id, path1, path2, &wg)
	go func3(date, id, path1, path2, &wg)
	go func4(date, id, path1, path2, &wg)
	go func5(date, id, path1, path2, &wg)
	wg.Wait()
}
```

```
$ go run logCollect.go -host <IP> -port <Port> -u <user> -p <pasword> -id <例：9999> -path1 /usr/~~~~/bin/ -path2 /home/~~~~/log_tool/
```

# 解説

今回のコードの簡単な説明をしていきます。
簡単だよこんなのって方は、せめて...せめて1個下のmoduleの宣伝でもしていただければなんて...

## Module

```
"github.com/ryojsb/paramigo"
```
こちらですが、今回のツールを作るにあたって、importして引数渡せばよろしくやってくれるもの欲しいなと思って作ったものです。

使い方は以下に記載してありますので、読んでみていただければ幸いです。
https://qiita.com/ryojsb/items/efcf9e06f57654570785

## main関数 start!!

```
func main() {
    flag.StringVar(&host, "host", "", "IP Address")
    flag.StringVar(&port, "port", "", "Port")
    flag.StringVar(&user, "u", "", "user")
    flag.StringVar(&password, "p", "", "Password")
    flag.StringVar(&id, "id", "", "S/N")
    flag.StringVar(&path1, "path1", "", "PATH for bin")
    flag.StringVar(&path2, "path2", "", "PATH for log file")
    flag.Parse()
```

`flaf.Parse()`によって、標準入力による実行commandのオプションの値を上の方で定義してある

```
var (
    host     string
    port     string
    user     string
    password string
    id       string
    path1    string
    path2    string
)
```
これらの変数に格納して、使えるようになります。

## Time

```
t := time.Now()
formattedTime := t.Format(format)
date := strings.ReplaceAll(formattedTime, "-", "")
```

ここですが

```
t := time.Now()
```
によって現在の時間を変数tに格納します。
例えばこれをPrintlnしてあげると以下のような出力結果が出てきます。

```
t := time.Now()
fmt.Println(t)           // => "2019-05-17 16:55:30.757800829 +0900 JST"
```

しかし、今回は2019-05-17の部分しか必要なかったので、上の方で定数を設定しました。

```
const (
    format = "2000-01-01"
)
```

この変数formatに対して以下のように実行すると、この形に合う部分を選んで今の時間に変換して返してくれます。
(constは変えられないよと思うかもしれませんが、ここでは定数であるformatをコピーして新しい変数を作って返してくれています。)

```
formattedTime := t.Format(format)
date := strings.ReplaceAll(formattedTime, "-", "")
```

あとは、ハイフン(-)が不要だったので、stringsライブラリのReplace関数を使って、文字列変換を行っています。


## Goroutine
最後ですね！あとひと踏ん張りお願いします。

```
var wg sync.WaitGroup
wg.Add(5)
go func1(date, id, path1, path2, &wg)
go func2(date, id, path1, path2, &wg)
go func3(date, id, path1, path2, &wg)
go func4(date, id, path1, path2, &wg)
go func5(date, id, path1, path2, &wg)
wg.Wait()
```
### sync.WaitGroup

`sync.WaitGroup`はとても便利ですね！
例えば、Goroutineがストップしないように各所に以下のようなものを埋め込む場合もあります。

```
time.Sleep(100 * time.Milliseconds)
```

ただこれをしようとすると、ある程度勘で何秒sleepさせようかな見たいな感じになっちゃいますよね。
勘が外れて、してほしかったことがされずに終わっちゃいましたなんて怖い....

そんな時にこれがとても役に立つのです！！！

### 使い方
使い方としては5ステップです
#### 1° WaitGroupの定義
```
var wg sync.WaitGroup
```

#### 2° WaitGroupにこの後何個のGoroutineが来るのかを教えてあげる。
```
wg.Add(5)
```

今回は「5個のコードを流したかった」
すなわち5個のGoroutineを用意したので、Add(5)をしています。

#### 3° Goroutineに入れる関数の中に終了のお知らせを組み込む。
```
func Func1(date string, id string, path1 string, path2 string, wg *sync.WaitGroup) {
    cmd := "BinPATH{Log出力command} > FilePATHDATE_ID_<file名>.txt"
    cmd = strings.ReplaceAll(cmd, "BinPATH", path1)
    cmd = strings.ReplaceAll(cmd, "FilePATH", path2)
    cmd = strings.Replace(cmd, "DATE", date, 1)
    cmd = strings.ReplaceAll(cmd, "ID", id)
    paramigo.InnerCommand(host, port, user, password, cmd)
    wg.Done()
}
```

関数の最後に`wg.Done()`どいうのがいますが、これはGoroutineでやりたいことを終わったよ！ってことを知らせてくれます。

ここでWaitGroupを渡す際、引数の変数定義が`pointer型`になっていることに注意をしてください。

#### 4° 関数にWaitGroupを渡す。

```
go func1(date, id, path1, path2, &wg)
```

3番でも解説した通り、関数での定義はpointer型なので、しっかりと`変数のアドレス`を渡してあげてください！

#### 5° Main関数の処理を止める門番を設置しよう
```
wg.Wait()
```

これがWaitGroupの要ですね！
`wg.Add()`の際に教えてあげた個数分、`wg.Done()`を受け取るまでずっとmainの処理を塞いでおいてくれるんです。

このおかげで、Goroutineがちゃんと動く前にmain関数が終わってしまうなんてことがなくなります。

### 関数の中身
ここはsshでremoteのサーバにcommandを送信しています。

```
cmd := "BinPATH{Log出力command} > FilePATHDATE_ID_<file名>.txt"
cmd = strings.ReplaceAll(cmd, "BinPATH", path1)
cmd = strings.ReplaceAll(cmd, "FilePATH", path2)
cmd = strings.Replace(cmd, "DATE", date, 1)
cmd = strings.ReplaceAll(cmd, "ID", id)
paramigo.InnerCommand(host, port, user, password, cmd)
```

関数内で定義したコマンドに対して、`strings.ReplaceALL()`と`strings.Replace()`を使って整形していきます。

サーバによって、PATHが違ったりするので、決め打ちできないということもあり、ここで変換していきます。

commandのbinがあるところから打てるように`BinPATH`を
どこのdirectory下にそのlogをテキスト形式で保存するかという`FilePATH`を

まず変換した後に、

main関数内で取得し、整形した実行時の日付
サーバに割り当てられたid

を埋め込んで(最終的に`yyyymmdd_id_command名.txt`のようなファイルを作るようにしてます。)

`paramigo`ライブラリの`InnerCommand()`にパラメータを渡して、Remoteサーバにcommandを打っています。


# まとめ
社会人2年目の新しいチャレンジとして、Golangを学習し始めて3週間ほどがたち、やっと使い勝手がわかりつつあるのかなと思っています。
これからもやったことなどを積極的に発信していければなと思うので、こうした方がいいよとかがありましたら、ぜひ教えて下さい。

よろしくお願いいたします。