はじめに
この記事はNetOpsCoding Advent Calendar 2015 の14日目の記事です。検証作業を捗らせるために一度に複数台のCiscoルータのポートを見える化するツールをRuby/Sinatra+SNMPで作成しました。
目的
検証作業では、複数台のルータのポートのリンク状態やトラフィック状態を都度確認する必要があります。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を取得しています。トラフィックは、平均トラフィック(bps, pps)を表示できる下記を使用しています。
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秒平均に変更されます。
interface FastEthernet1
load-interval 30
end
スクリプト
ポート見える化ツールは下記のファイルで構成されています。
├── Gemfile -- RubyGemsの依存関係記述ファイル
├── views
│ └── index.slim -- HTMLテンプレートファイル
└── webport.rb -- 実行ファイル
Gemfile
はBundleでRubyGemsの依存関係を記述したファイルです。
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
メソッドを定義して、トラフィックを単位付きで表示するようにしています。
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ディレクトリ配下に入れる必要があります。
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
|  
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サーバが起動します。
$ 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にアクセスすると、ポートの状態を一括して確認できます。画面を更新すると最新の状態を確認できます。
表形式でルータ単位に表示します。表は左から順に下記のようになっています。
- #:ポートの順番
- PortName(long):インタフェース名(ifDescr)
- PortName(short):インタフェース名(ifName)
- Description:インタフェースのdescription(ifAlias)
- AdminStatus:インタフェースのリンク状態
- OperStatus:インタフェースのプロトコル状態
- in(bps):トラフィック(IN方向)
- in(pps):パケット数(IN方向)
- out(bps):トラフィック(OUT方向)
- out(pps):パケット数(OUT方向)
トラフィックとパケット数で値が取得できなかったところは空白で表示しています。
おわりに
監視システムの場合、ポート状態のポーリングまで待たないといけません。今回試作したツールは小規模な環境を対象としているため、リアルタイムな情報反映が可能です。ルータの台数が多くなる場合、同時取得数を増やすことで時間を短縮することが可能です。
実際に検証で利用し、リンクダウンしているポートがひと目でわかるようになりました。トラフィックの表示もできるため、リンクアグリゲーションで、どのポートにトラフィックが流れているか確認できたため、検証が捗りました。
実際の検証時には、Loopbackインタフェースやサブインタフェースの情報は不要のため、スクリプト中でフィルタリングして表示するようにするなど、目的に合わせてカスタマイズしています。