CiscoルータのルーティングテーブルをRuby/Sinatraで見える化して検証捗らせてみた

# はじめに
この記事は[NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding) の22日目の記事です。検証作業を捗らせるために一度に複数台のCiscoルータのルーティングテーブルを見える化するツールをRuby/Sinatra+SNMPで試作しました。
![WebRoutes.png](https://qiita-image-store.s3.amazonaws.com/0/12026/7418f156-b5be-045f-3041-fae09740c10c.png)


# 目的
検証作業では、複数台のルータのルーティングテーブルを都度確認する必要があります。ネットワークエンジニアなら各ルータにTELNET/SSHしてshow ip routeコマンドを打つのは手馴れていると思います。

ただ、ルータの台数が多くなると結構面倒です。マクロなどでshow ip routeを自動実行してもいいですが、CLIではルーティングテーブルを確認するのは大変です。GUIでルーティングテーブルを確認しようと考えた結果、WEBブラウザでルーティングテーブルを見える化するツールを試作しました。

ルータのルーティングテーブルを正規化して表形式で表示することで見やすくします。実装にあたっては、show ip routeを正規表現で抽出するのは至難の業なので、SNMPを使って抽出することにしました。

# 概要
使用している技術は[NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding)の14日目の[CiscoルータのポートをRuby/Sinatraで見える化して検証捗らせてみた](http://qiita.com/kooshin/items/bd214755be534d850c5f)と同等となります。そのため、実行環境等はまったく同じです。

- 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デザイン）

今回はグローバルルーティングテーブル（address-family ipv4）のルーティングテーブルを見える化します。ルーティング情報は標準MIBのIP-FORWARD-MIBの[ipCidrRouteEntry](http://tools.cisco.com/Support/SNMP/do/BrowseOID.do?local=ja&translate=Translate&objectInput=1.3.6.1.2.1.4.24.4.1#oidContent)を用いています。

```rb:ipCidrRouteEntry
  '1.3.6.1.2.1.4.24.4.1.5', # IP-FORWARD-MIB::ipCidrRouteIfIndex
  '1.3.6.1.2.1.4.24.4.1.1', # IP-FORWARD-MIB::ipCidrRouteDest
  '1.3.6.1.2.1.4.24.4.1.2', # IP-FORWARD-MIB::ipCidrRouteMask
  '1.3.6.1.2.1.4.24.4.1.4', # IP-FORWARD-MIB::ipCidrRouteNextHop
  '1.3.6.1.2.1.4.24.4.1.6', # IP-FORWARD-MIB::ipCidrRouteType
  '1.3.6.1.2.1.4.24.4.1.8', # IP-FORWARD-MIB::ipCidrRouteAge
```

# スクリプト
ルーティングテーブル見える化ツールは下記のファイルで構成されています。

```bash:ファイル構成
├── Gemfile         -- Ruby依存関係の記述ファイル
├── views           
│   └── index.slim  -- HTMLテンプレートファイル（slim形式）
└── webroutes.rb    -- 実行ファイル本体兼Webサーバ
```

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

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

``webroutes.rb``は実行ファイル本体です。設定箇所は``HOSTS``変数と``COMMUNITY``変数です。``HOSTS``変数で対象ルータを指定しています。``COMMUNITY``変数でSNMPのコミュニティ名を指定しています。Rubyのparallelライブラリを使用して、``in_threads: 4``を指定することで、同時に4台のルータでSNMPの値を取得します。

```rb:webroutes.rb
require 'sinatra'
require 'sinatra/reloader'
require 'slim'
require 'snmp'
require 'parallel'
require 'ipaddress'

set :bind, '0.0.0.0'

HOSTS = %w(192.168.88.101 192.168.88.102)
COMMUNITY = 'public'
WALK_COLUMNS = [
  '1.3.6.1.2.1.4.24.4.1.5', # IP-FORWARD-MIB::ipCidrRouteIfIndex
  '1.3.6.1.2.1.4.24.4.1.1', # IP-FORWARD-MIB::ipCidrRouteDest
  '1.3.6.1.2.1.4.24.4.1.2', # IP-FORWARD-MIB::ipCidrRouteMask
  '1.3.6.1.2.1.4.24.4.1.4', # IP-FORWARD-MIB::ipCidrRouteNextHop
  '1.3.6.1.2.1.4.24.4.1.6', # IP-FORWARD-MIB::ipCidrRouteType
  '1.3.6.1.2.1.4.24.4.1.8', # IP-FORWARD-MIB::ipCidrRouteAge
]
Route = Struct.new(:ifindex, :dest, :mask, :nexthop, :type, :age, :prefix, :time, :iface)
Host = Struct.new(:name, :routes, :ifaces)

get '/' do
  @hosts = []
  Parallel.each(HOSTS, in_threads: 4) do |host|
    begin
      SNMP::Manager.open(host: host, community: COMMUNITY, timeout: 1, retries: 3) do |manager|
        ifaces = {}
        manager.walk('ifDescr') do |vb|
          index = vb.name.last.to_s
          value = vb.value.to_s
          ifaces[index] = value
        end

        routes = []
        manager.walk(WALK_COLUMNS) do |row|
          values = row.map { |vb| vb.value.to_s }
          route = Route.new(*values)
          route.prefix = IPAddress("#{route.dest}/#{route.mask}").to_string
          route.iface = ifaces[route.ifindex]
          route.time = Time.at(Time.now.to_i - route.age.to_i)
          routes << route
        end

        @hosts << Host.new(host, routes, ifaces)
      end
    rescue => e
      @hosts << Host.new("#{host} - #{e}", [], {})
    end
  end
  @hosts.sort_by!(&:name)

  slim :index
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" crossorigin="anonymous"
    title WebRoutes
  body
    - @hosts.each do |host|
      h3
        = host.name
        small
          | &nbsp;Global Routing Table
      table.table.table-condensed.table-striped.table-hover
        thead
          th #
          th Prefix
          th Dest
          th Mask
          th Nexthop
          th Port
          th Since
        tbody
          - host.routes.each_with_index do |route, index|
            tr
              td = index + 1
              td = route.prefix
              td = route.dest
              td = route.mask
              td = route.nexthop
              td = route.iface
              td = route.time
    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" crossorigin="anonymous"
```

# 実行結果
```bash:実行結果
$ bundle
Using backports 3.6.7
Using ipaddress 0.8.0
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, 14 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
$ ruby webroutes.rb
[2015-12-06 00:08:35] INFO  WEBrick 1.3.1
[2015-12-06 00:08:35] 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-06 00:08:35] INFO  WEBrick::HTTPServer#start: pid=30233 port=4567
```

Webブラウザから[http://localhost:4567](http://localhost:4567)にアクセスすると、経路の状態を一括して確認できます。
画面を更新すると最新の状態を確認できます。
![WebRoutes.png](https://qiita-image-store.s3.amazonaws.com/0/12026/7418f156-b5be-045f-3041-fae09740c10c.png)

## 表の見方

- Prefix : 経路をプレフィックス形式で表しています。
- Dest : 経路のネットワークアドレスを表しています。
- Mask : 経路のサブネットマスクを表しています。
- Nexthop : 経路のネクストホップアドレスを表しています。0.0.0.0の場合はは自分自身を表します。
- Port : 経路のネクストホップのポートを表しています。
- Since : 経路を学習した時刻を表しています。

## 工夫点
1. 経路学習のタイミングもルータ内で保持していますが、現在時刻から何秒前など見難いため、
実際の学習時刻に変換して見やすくしています。SNMPの取得タイミングによって1秒程度誤差がでます。
2. 見やすいプレフィックス形式で表示しています。
3. 表形式で表示しているため、show ip routeよりも若干見やすくなっています。
4. 一度に複数のルータでSNMP取得するため、Parallelを用いて多重化しています。

# おわりに
今回は、グローバルルーティングテーブルを表示させる機能を試作しています。実際の検証で作成したものはBGPのVPNv4を表示させるツールでしたが、あまり一般的ではないと思い作りなおし、必要最低限の機能のみ実装しています。

# 参考
- [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