Ruby/XMPPでCiscoルータとチャットしてみた

# はじめに
この記事は[NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding) の17日目の記事です。[ネットワークプログラマビリティ勉強会 #6](http://network-programmability.connpass.com/event/19603/)の[ルータとチャットしてみました](http://www.slideshare.net/OkuyaIgarashi/router-chat-for-np)に触発され、Ruby/XMPPでCiscoルータとチャットしてみました。
![chat1.png](https://qiita-image-store.s3.amazonaws.com/0/12026/383986ae-0ebc-f331-2fe3-bf32c96a1861.png)


# 概要
インスタント・メッセンジャープロトコルのXMPPを用いて、Ciscoルータ1台毎にアカウントを作成して、チャットしてみました。環境は下記のとおりです。XMPPサーバ、XMPPクライアント、XMPPボットはローカルホスト上で動作させるため、アカウントのドメイン名は@localhostで統一しています。

![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/12026/66ce2f50-1ed3-6d4c-cb87-2f33ac52aaf4.png)

- XMPPサーバ：[Openfire](http://www.igniterealtime.org/projects/openfire/) in Ubuntu 15.04
- XMPPクライアント：[Empathy](https://wiki.gnome.org/Apps/Empathy)（GNOMEのインスタント・メッセンジャー）
- XMPPボット：自作Rubyスクリプト（xmpp4rとexpect4rライブラリを使用）
- Ciscoルータ：Cisco1812J 2台

## XMPPサーバ
XMPPサーバはDockerコンテナ上に構築しました。``Dockerfile``は[sameersbn/openfire:3.10.3-1](https://github.com/sameersbn/docker-openfire)を利用させていただきました。セットアップ方法については割愛します。利用した機能はUsersとGroupsのみです。今回はGroup Chatは利用していません。

![openfire.png](https://qiita-image-store.s3.amazonaws.com/0/12026/fa0fec8c-59f8-9076-bc48-7ed6824eb284.png)

### アカウント
アカウントについては下記の通り管理者ユーザのadminと、Ciscoルータを示すr1とr2のアカウントを作成しました。Routersグループを作成し、すべてのアカウントを所属させています。これにより連絡先リストを共有することができます。

- admin@localhost - XMPPクライアントを起動するためのアカウント
- r1@localhost/bot - CISCOルータ R1
- r2@localhost/bot - CISCOルータ R2

## XMPPクライアント
XMPPクライアントはUbuntuに標準で付いているインスタント・メッセンジャーアプリのEmpathyを利用しました。下記の通り、adminアカウントでログインすると、2台のルータが連絡先リストに出現します。アカウントのアイコンはEmpathyに手動で登録しました。アイコンはCiscoのアイコンを利用しています。
![empathy.png](https://qiita-image-store.s3.amazonaws.com/0/12026/f363851a-7fe9-d039-3c28-19f7683269c9.png)

## XMPPボット
XMPPボットはRuby2.2.3で作成しました。ライブラリは下記を使用して実装しています。スクリプトのほとんどは各ライブラリのサンプルを用いて実装しています。

- [xmpp4r](https://rubygems.org/gems/xmpp4r)：XMPP用ライブラリ
- [expect4r](https://rubygems.org/gems/expect4r/)：Ciscoルータ接続用ライブラリ

### スクリプト
``bot.rb``が実行ファイルです。``HOSTS``変数に配列でアカウント名とルータのIPアドレスを登録しています。``USERNAME``、``PASSWORD``、``ENABLE_PASSWORD``はCiscoルータにTELNETする際のログイン情報を設定します。``BOT_PASSWORD``はXMPPサーバに登録されたアカウントのパスワードを示します。

スクリプトの動作は単純です。ルータごとにBotクラスの``start``メソッドが実行され、XMPPサーバと接続し、XMPPクライアントからのメッセージを待ち受けします。XMPPクライアントからメッセージを受信すると``on_message_callback``メソッドが実行されます。メソッド内でshow、ping、traceから始まるメッセージはコマンドと解釈して、ルータでコマンドを実行し、その実行結果をXMPPクライアントに返信しています。``reply_cmd``メソッドがルータにTELNETしてコマンドを実行しています。実行した結果は``reply``メソッドで、送信元のXMPPクライアントに返信します。

```rb:bot.rb
require 'xmpp4r'
require 'expect4r'
require 'logger'

Log = Logger.new(STDOUT)
Log.level = Logger::DEBUG

HOSTS = [
  %w(R1 192.168.88.101),
  %w(R2 192.168.88.102)
]
USERNAME        = 'cisco'
PASSWORD        = 'cisco'
ENABLE_PASSWORD = 'cisco'
BOT_PASSWORD    = 'cisco'

# Bot for Cisco IOS
class Bot
  def initialize(host, ipaddr)
    @host = host
    jid = Jabber::JID.new("#{host}@localhost/bot")
    @client = Jabber::Client.new(jid)
    @ios = Expect4r::Ios.new_telnet(
      host: ipaddr,
      user: USERNAME,
      pwd: PASSWORD,
      enable_password: ENABLE_PASSWORD
    )
  end

  def start
    Log.info "#{@host} starting"
    @client.connect
    @client.auth(BOT_PASSWORD)
    @client.send(Jabber::Presence.new.set_show(:chat).set_type(:available))
    @client.add_message_callback(&method(:on_message_callback))
  end

  def reply(msg, reply_content)
    reply_msg = Jabber::Message.new(msg.from, reply_content)
    reply_msg.type = msg.type
    @client.send(reply_msg)
  end

  def reply_cmd(msg, cmd)
    Thread.new do
      begin
        Log.debug "#{@host} #{msg.from} [#{cmd}]"
        @ios.login
        result = @ios.exec(cmd)
        reply(msg, result.join)
      rescue => e
        reply(msg, "コマンド実行失敗 :'( #{e}")
      end
    end
  end

  def on_message_callback(msg)
    return if msg.body.nil? || msg.type == :error
    case msg.body
    when /^((show|ping|trace).*)/
      cmd = $1
      reply(msg, "『#{cmd}』実行！:-)")
      reply_cmd(msg, cmd)
    else
      reply(msg, 'show|ping|traceから始まるコマンドのみサポート:-(')
    end
  end
end

if __FILE__ == $PROGRAM_NAME
  HOSTS.each do |host, ipaddr|
    Bot.new(host, ipaddr).start
  end
  Thread.stop
end
```

### 実行結果

XMPP用のボットスクリプトを実行。R1とR2のアカウントでXMPPサーバに接続し、XMPPクライアントからのメッセージを待ち受けします。メッセージを受け取ると、送信元のアカウント名とコマンドを表示します。

```bash:実行結果
$ ruby bot.rb
I, [2015-12-08T23:56:14.884924 #16863]  INFO -- : R1 starting
I, [2015-12-08T23:56:15.224579 #16863]  INFO -- : R2 starting
D, [2015-12-08T23:56:16.813239 #16863] DEBUG -- : R1 admin@localhost/2a20c564 [show ip route]
D, [2015-12-08T23:56:20.036932 #16863] DEBUG -- : R1 admin@localhost/2a20c564 [show int desc]
D, [2015-12-08T23:56:24.352473 #16863] DEBUG -- : R1 admin@localhost/2a20c564 [show ip route]
D, [2015-12-09T00:00:04.743047 #16863] DEBUG -- : R1 admin@localhost/2a20c564 [show vrf]
```

XMPPクライアントからルータを選択して、コマンドを送信すると下記のように実行結果が返ってきます。
![chat2.png](https://qiita-image-store.s3.amazonaws.com/0/12026/2681c23d-cc3e-66d4-a84c-474f59f032d4.png)

対応しているコマンドはshow、ping、traceのみとなります。copy running-config startup-configコマンドのようにプロンプトが返ってくるコマンドには未対応です。

# おわりに
Ciscoルータとチャットできました。ちょっとしたコマンドの実行をしたい場合など、いちいちログインしなくても、コマンドを実行できるので便利です。ただし、補完機能がないため、CLIの恩恵を受けられません。

# 参考
- [Rubyで多数のCiscoルータで複数のコマンドを実行する](http://qiita.com/kooshin/items/fe077630aad78b02a88f) - Qiita
- [Ciscoルータのポート一覧をRubyとSNMPで取得する](http://qiita.com/kooshin/items/dcba6799020c677de8ab) - Qiita
- [CiscoルータのポートをRuby/Sinatraで見える化して検証捗らせてみた](http://qiita.com/kooshin/items/bd214755be534d850c5f) - Qiita
- [ルータとチャットしてみました](http://www.slideshare.net/OkuyaIgarashi/router-chat-for-np) - [ネットワークプログラマビリティ勉強会 #6](http://network-programmability.connpass.com/event/19603/)
- [xmpp4r](https://rubygems.org/gems/xmpp4r) - RubyGems.org
- [expect4r](https://rubygems.org/gems/expect4r/) - RubyGems.org