CiscoルータのポートをRuby/Sinatraで見える化して検証捗らせてみた

# はじめに
この記事は[NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding) の14日目の記事です。検証作業を捗らせるために一度に複数台のCiscoルータのポートを見える化するツールをRuby/Sinatra+SNMPで作成しました。
![WebPort.png](https://qiita-image-store.s3.amazonaws.com/0/12026/f383ad75-d7ea-1ba8-03dd-60011c9553ff.png)


# 目的
検証作業では、複数台のルータのポートのリンク状態やトラフィック状態を都度確認する必要があります。Ciscoルータの場合、ルータごとにTELNET/SSHしてshow interfaceを打てば確認できますが、ルータの台数が増えると手間と時間がかかります。コマンドを自動実行するツールで確認でも、一気に確認できますが、CLIではひと目で確認することが困難です。

CactiやZabbixなど監視システムを構築することで、ひと目で確認することができるようになりますが、しかしながら小規模な検証作業ではわざわざ監視システムを構築することに手間がかかります。そこで、小規模な検証を捗らせるために、ルータのポートを見える化するツールを作成しようと思いました。

CLIではポートの状態を確認するために文字をチェックする必要があり不便です。そのため、GUIでひと目でわかるツールにしようと考えた結果、WEBブラウザでルータのポートを見える化するツールを試作しました。

# 概要
見える化ツールは下記の環境で試作しました。

- Cisco 1812J
- Ubuntu 15.04
- Ruby 2.2.3
- snmp 1.2.0（RubyでSNMP）
- sinatra 1.4.6（RubyのWebフレームワーク）
- slim（RubyのHTMLテンプレートエンジン）
- bootstrap3（HTML/CSSデザイン）

ポートのリンク状態は標準MIB、トラフィックはCiscoのプライベートMIBの[OLD-CISCO-INTERFACES-MIB](http://tools.cisco.com/Support/SNMP/do/BrowseOID.do?local=ja&translate=Translate&objectInput=1.3.6.1.4.1.9.2.2.1.1)を取得しています。トラフィックは、平均トラフィック（bps, pps）を表示できる下記を使用しています。

```rb:OLD-CISCO-INTERFACES-MIB
locIfInBitsSec  : 1.3.6.1.4.1.9.2.2.1.1.6
locIfInPktsSec  : 1.3.6.1.4.1.9.2.2.1.1.7
locIfOutBitsSec : 1.3.6.1.4.1.9.2.2.1.1.8
locIfOutPktsSec : 1.3.6.1.4.1.9.2.2.1.1.9
```

上記のMIBで取得できるトラフィックは標準では5分平均です。もうちょっと早い間隔で確認したい場合には、下記のようにCiscoルータのインタフェースで``load-interval 30``を設定すると30秒平均に変更されます。

```bash:cisco
interface FastEthernet1
 load-interval 30
end
```

# スクリプト

ポート見える化ツールは下記のファイルで構成されています。

```bash:ファイル構成
├── Gemfile        -- RubyGemsの依存関係記述ファイル
├── views
│   └── index.slim -- HTMLテンプレートファイル
└── webport.rb     -- 実行ファイル
```

``Gemfile``はBundleでRubyGemsの依存関係を記述したファイルです。

```rb:Gemfile
source 'https://rubygems.org'
gem 'snmp'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'slim'
gem 'parallel'
gem 'activesupport'
```

``webport.rb``は実行ファイル本体です。設定箇所は``HOSTS``変数と``COMMUNITY``変数です。``HOSTS``変数で対象ルータを指定しています。``COMMUNITY``変数でSNMPのコミュニティ名を指定しています。Rubyのparallelライブラリを使用して、``in_threads: 4``を指定することで、同時に4台のルータでSNMPの値を取得します。SNMPで値が取得できなかったものはポート名にN/Aを指定した情報を表示します。画面用のヘルパ``number_to_traffic``メソッドを定義して、トラフィックを単位付きで表示するようにしています。

```rb:webport.rb
require 'sinatra'
require 'sinatra/reloader'
require 'slim'
require 'snmp'
require 'active_support/all'
require 'parallel'
include ActiveSupport::NumberHelper
set :bind, '0.0.0.0'

HOSTS = %w(192.168.88.101 192.168.88.102)
COMMUNITY = 'public'

MAP_ifAdminStatus = Hash[*%w(1 up 2 down 3 testing)]
MAP_ifOperStatus  = Hash[*%w(1 up 2 down 3 testing 4 unknown 5 dormant 6 notPresent 7 lowerLayerDown)]

WALK_COLUMNS = [
  'ifDescr', 'ifName', 'ifAlias', 'ifAdminStatus', 'ifOperStatus',
  '1.3.6.1.4.1.9.2.2.1.1.6', # OLD-CISCO-INTERFACES-MIB::locIfInBitsSec
  '1.3.6.1.4.1.9.2.2.1.1.7', # OLD-CISCO-INTERFACES-MIB::locIfInPktsSec
  '1.3.6.1.4.1.9.2.2.1.1.8', # OLD-CISCO-INTERFACES-MIB::locIfOutBitsSec
  '1.3.6.1.4.1.9.2.2.1.1.9', # OLD-CISCO-INTERFACES-MIB::locIfOutPktsSec
]
Host = Struct.new(:name, :ports)
Port = Struct.new(:descr, :name, :alias, :admin_status, :oper_status, :in_bps, :in_pps, :out_bps, :out_pps)

get '/' do
  @hosts = []
  Parallel.each(HOSTS, in_threads: 4) do |host|
    ports = []
    begin
      SNMP::Manager.open(host: host, community: COMMUNITY, timeout: 1, retries: 3) do |manager|
        manager.walk(WALK_COLUMNS) do |row|
          values = row.map { |vb| vb.value.to_s }
          values.map! { |v| v == 'noSuchInstance' ? -1 : v }
          port = Port.new(*values)
          port.admin_status = MAP_ifAdminStatus[port.admin_status]
          port.oper_status = MAP_ifOperStatus[port.oper_status]
          ports << port
        end
      end
    rescue
      ports << Port.new('N/A', 'N/A', 'N/A', 'down', 'down', -1, -1, -1, -1)
    end
    @hosts << Host.new(host, ports)
  end
  @hosts.sort_by!(&:name)
  slim :index
end

helpers do
  def number_to_traffic(traffic, unit)
    traffic == -1 ? '' : number_to_delimited(traffic) + ' ' + unit
  end
end
```

``views/index.slim``はWeb画面用のHTMLテンプレートファイルです。Bootstrap3を利用して、見た目をちょっとかっこよくしています。Boostrap3やjQueryはCDNのファイルを参照するようにしています。検証環境からインターネットに接続できない場合は、ローカルに各種ファイルをダウンロードしてpublicディレクトリ配下に入れる必要があります。

```slim:views/index.slim
doctype html
html lang="ja"
  head
    meta charset="utf8"
    meta http-equiv="X-UA-Compatible" content="IE=edge"
    meta name="viewport" content="width=device-width, initial-scale=1"
    link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"
    title WebPort
  body
    - @hosts.each do |host|
      h3
        = host.name
        | &nbsp
        small
          | Ports
      table.table.table-condensed.table-striped.table-hover
        thead
          th.text-right #
          th PortName(long)
          th PortName(short)
          th Description
          th AdminStatus
          th OperStatus
          th.text-right In(bps)
          th.text-right In(pps)
          th.text-right Out(bps)
          th.text-right Out(pps)
        tbody
          - host.ports.each_with_index do |port, index|
            tr class=(port.descr == 'N/A' ? 'danger' : '')
              td.text-right = index + 1
              td = port.descr
              td = port.name
              td = port.alias
              td class=(port.admin_status == 'down' ? 'danger' : '')
                = port.admin_status
              td class=(port.oper_status == 'down' ? 'danger' : '')
                = port.oper_status
              td.text-right
                = number_to_traffic(port.in_bps, 'bps')
              td.text-right
                = number_to_traffic(port.in_pps, 'pps')
              td.text-right
                = number_to_traffic(port.out_bps, 'bps')
              td.text-right
                = number_to_traffic(port.out_pps, 'pps')
    script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
    script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"
```

# 実行結果
ライブラリをbundleコマンドでインストールし、実行ファイルwebport.rbをbundle execコマンドで実行しています。問題なく実行できるとTCP4567ポートでWebサーバが起動します。

```bash:実行結果
$ bundle
Using i18n 0.7.0
Using json 1.8.3
Using minitest 5.8.3
Using thread_safe 0.3.5
Using tzinfo 1.2.2
Using activesupport 4.2.5
Using backports 3.6.7
Using multi_json 1.11.2
Using parallel 1.6.1
Using rack 1.6.4
Using rack-protection 1.5.3
Using rack-test 0.6.3
Using tilt 2.0.1
Using sinatra 1.4.6
Using sinatra-contrib 1.4.6
Using temple 0.7.6
Using slim 3.0.6
Using snmp 1.2.0
Using bundler 1.10.6
Bundle complete! 6 Gemfile dependencies, 19 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
$ bundle exec ruby webport.rb
[2015-12-09 23:29:40] INFO  WEBrick 1.3.1
[2015-12-09 23:29:40] INFO  ruby 2.2.3 (2015-08-18) [x86_64-linux]
== Sinatra (v1.4.6) has taken the stage on 4567 for development with backup from WEBrick
[2015-12-09 23:29:40] INFO  WEBrick::HTTPServer#start: pid=32655 port=4567
```

Webブラウザから[http://localhost:4567](http://localhost:4567)にアクセスすると、ポートの状態を一括して確認できます。画面を更新すると最新の状態を確認できます。

![WebPort.png](https://qiita-image-store.s3.amazonaws.com/0/12026/8fd3f878-08d7-a801-6ca5-7e57c708d3f8.png)

表形式でルータ単位に表示します。表は左から順に下記のようになっています。

- #：ポートの順番
- PortName(long)：インタフェース名（ifDescr）
- PortName(short)：インタフェース名（ifName）
- Description：インタフェースのdescription（ifAlias）
- AdminStatus：インタフェースのリンク状態
- OperStatus：インタフェースのプロトコル状態
- in(bps)：トラフィック（IN方向）
- in(pps)：パケット数（IN方向）
- out(bps)：トラフィック（OUT方向）
- out(pps)：パケット数（OUT方向）

トラフィックとパケット数で値が取得できなかったところは空白で表示しています。

# おわりに
監視システムの場合、ポート状態のポーリングまで待たないといけません。今回試作したツールは小規模な環境を対象としているため、リアルタイムな情報反映が可能です。ルータの台数が多くなる場合、同時取得数を増やすことで時間を短縮することが可能です。

実際に検証で利用し、リンクダウンしているポートがひと目でわかるようになりました。トラフィックの表示もできるため、リンクアグリゲーションで、どのポートにトラフィックが流れているか確認できたため、検証が捗りました。

実際の検証時には、Loopbackインタフェースやサブインタフェースの情報は不要のため、スクリプト中でフィルタリングして表示するようにするなど、目的に合わせてカスタマイズしています。

# 参考
- [Rubyで多数のCiscoルータで複数のコマンドを実行する](http://qiita.com/kooshin/items/fe077630aad78b02a88f)
- [Ciscoルータのポート一覧をRubyとSNMPで取得する](http://qiita.com/kooshin/items/dcba6799020c677de8ab)