シスコルータのチャットを自動化する

[NetOpsCoding Advent Calendar 2016](http://qiita.com/advent-calendar/2016/netopscoding) 12/16分の記事です。

# 背景

もともとChatOps系のネタで何をやろうかと考えていたのですが、IOS自身にもチャットに近い機能(`send`コマンド)があることを思い出しました。

```text:送信側がこれを実行すると
Router#send *
Enter message, end with CTRL/Z; abort with CTRL/C:
test message
^Z
Send message? [confirm]
Router#
```

```text:受信側でこれが表示される
***
***
*** Message from tty1 to all terminals:
***
test message
```

で、この`send`コマンドがとても使いづらいんです。

* 発言の度にログインするのが非常に手間である。
* ログインしてないと発言を受けることができない。
* 過去のメッセージが記録されない。
* 日本語や絵文字が使えない。
* チャットだったらGUIの方が使いやすい。
* アバター画像があると親近感あって良い。

なので、これらの問題を自動化その他もろもろで何とかしてみました。

# 成果物(Web UI部分)

![chat.png](https://qiita-image-store.s3.amazonaws.com/0/104608/4e1f047c-698d-4a86-a676-231e571020cf.png)

全然`send`コマンドの面影がありませんが、バックエンドでは重要な役割を担っています。
裏の仕組みについては後述します。

CLI部分を完全に自動化したので、非常に快適にチャットができるようになりました。

[https://github.com/tanksuzuki/cchat](https://github.com/tanksuzuki/cchat)

# 構成

![構成.png](https://qiita-image-store.s3.amazonaws.com/0/104608/c03a0265-8839-050a-74bb-6b69f8f4fa2c.png)


## Router(as a Database)

標準の`send`ではログイン中のユーザにしか発言できず、過去の発言は保存されません。
そのため、データを保持できる何かしらの場所が必要となります。

### 案1: Flashにファイルを置く

Flashにファイルを作って、 IOS.shで発言内容を記録してく方法です。
[Cisco IOS シェル CLI モジュールの設定例](http://www.cisco.com/cisco/web/support/JP/111/1118/1118842_116208-configure-ios-00.html)

```text:echoやcatで繋げる例
Router#echo test1 > flash:file1

Router#echo test2 > flash:file2

Router#cat flash:file1 flash:file2
test1test2Router#
```

なんか普通すぎて面白みに欠けるため、この案は不採用になりました。

### 案2: bannerに記録する

`banner motd`的なところに発言内容を追記していく方式です。
容量的に懸念があったので、ちょっと検証してみました。

```text:motd容量チェック
Router(config)#banner ^
Enter TEXT message.  End with the character '^'.
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
----- 省略 -----
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
1234567890123456789012345678901234567
Maximum Banner size 3000 bytes reached. Banner message is truncated
Router(config)#$23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
^
% Invalid input detected at '^' marker.
```

3000byteでアウト。容量的に辛いので、この案は不採用になりました。

### 案3: ログに記録する

`send`は、ユーザに直接メッセージを送るだけでなく、ログに任意の文字列を出力させることもできます。

```
Router#send log hogehoge
Router#sh log | tail 1
Dec 12 2016 03:48:11: %SYS-7-USERLOG_DEBUG: Message from tty1(user id: hello@tanksuzuki.com): hogehoge

Router#
```

これは素晴らしい。

* WriteもReadもコマンド化されてる。
* バッファから溢れたログは自動破棄されるので管理が楽。
* バッファを増やすことで、ある程度の量の発言を保持できる。

ということで、ログ領域をNoSQL的に使うことに決めました。
JSONを突っ込んでしまいましょう。

## SSHクライアント

役割は2つです。

* APIで渡されたメッセージを、ルータにログインして`send log`する。
* APIでメッセージ取得要求を受けたら、ルータにログインして`sh log`する。

メッセージ構造は下記の通り。
この構造体をJSON化してログに突っ込むんですが、`send log`時にダブルクォーテーションが削られてしまうため、全体をBase64化してJSON構文を保護しています。

```go:cchat.go
type Message struct {
	ID    string `json:"id"`
	Date  string `json:"date"`
	Owner string `json:"owner"`
	Body  string `json:"body"`
}
```

下記は入れた結果です。

```text:show_log
Dec 12 2016 04:28:35: %SYS-7-USERLOG_DEBUG: Message from tty1(user id: hello@tanksuzuki.com): eyJpZCI6IjZiN2YzZmQ2YTliZjM1ZWFkY2U5NjE1OGQ3YjEzYzI2ZjE0ZDM3ZDAiLCJkYXRlIjoiMjAxNi0xMi0xMlQwNDoyODozM1oiLCJvd25lciI6ImhlbGxvQHRhbmtzdXp1a2kuY29tIiwiYm9keSI6InRlc3QifQ==
```

## API / Web UI

inputに入れた文章は、APIサーバの`POST /api/message`経由で　SSHクライアントに渡されます。
また、30秒ごとにAPIサーバの`GET /api/messages`を叩いてメッセージ一覧を取得し、Riotで描画しています。

アバター画像はGravatarから取得しています。
Gravatarに登録しているメールアドレスを、SSHのユーザ名にすればOKです。

# 課題

大きなところで3点あります。

## 課題1: 発言データは揮発性

ログはメモリに乗っているだけなので、再起動すると揮発します。
ログ領域をDB化する以上、半ば最初から諦めていた部分ですが、できることなら何とかしたいです。

## 課題2: ログの文字数制限

ログ1行あたりの文字数上限が340文字でした。
それ以上の文字数を`send log`しても、溢れた部分が削られるだけでコマンドとしては正常終了します。

長い文章を送るとJSONの後半部分が落ちて不良レコード化するため、現状だと半角40文字くらいまでがメッセージの上限になっています。

## 課題3: Read/Writeが遅すぎる

ストレスを感じるレベルで読み書きに時間がかかります。
ルータをDBにするのはやめた方が良いかもしれません。

# 今後について

まだ構想段階ですが、クライアントとDB(ルータ)の間に挟むコントローラ的なものがあれば劇的な改善が見込めそうです。

* 複数のDB(ルータ)に対し、RAID5的にレコードを分散配置して耐障害性UP。
* 長いレコードは1行に収まる程度に細切れにして分散配置し、コントローラ上で結合処理。
* Writeを非同期化し、Readはコントローラ上のキャッシュで高速化する。

今度作ります。