CiscoのスイッチのVLAN割当をSNMPで可視化する

created: 2015/12/24 12:47

Raspberry Piが3台になって元々置いてたルータのポート数が足りなくなってきたので、勉強ついでに秋葉原でCisco Catalyst 2960-8TC-L、L2スイッチの中古を買ってきた。お値段8000円。
とりあえずtelnetでスイッチに繋いで簡単なVLANの設定をして遊んでたんだけど、最近こういうAdvent Calendarがあることを知った。

NetOpsCoding Advent Calendar 2015

最高。
なるほどスイッチやルータを複数台扱う人は、telnetやなんやかんやを通してスクリプトで自動化しちゃうのですねと感動したので、それに触発されて作ってみたのが、タイトルにあるVLAN割当可視化くん。
とりあえずRubyを使って、いい感じにWebで見れるようになればいいよねくらいの気持ちで始めることに。

telnetで情報の取得を試みる

スイッチにはtelnet経由で接続できるので、試しに手で繋いでVLANの状態を見てみる。

Switch>enable
Password:
Switch#show vlan

VLAN Name                             Status    Ports
---- -------------------------------- --------- -------------------------------
1    default                          active    Fa0/1, Fa0/2, Fa0/3, Fa0/4, Gi0/1
2    port5-8                          active    Fa0/5, Fa0/6, Fa0/7, Fa0/8
1002 fddi-default                     act/unsup
1003 token-ring-default               act/unsup
1004 fddinet-default                  act/unsup
1005 trnet-default                    act/unsup

要はこの出力をパースすればいいんだけど、ヒューマンリーダブルになってるせいで逆に辛い。
名前とかステータスの変更で列幅が変わるのが容易に想像できるし、ちょっとこれをパースする気にはなれなかったのでtelnet経由以外の方法でVLANの割当を取得することに。

SNMPでVLANの情報を取得する

というわけでSNMPを試す。
CiscoのSNMP オブジェクトナビゲータを見ると、VLANの割当可視化にあたっては以下のOIDを参照すればいけそう。

OID 概要
1.3.6.1.4.1.9.9.46.1.3.1.1.3 VlanType。1ならイーサネットとか。これを基準にイーサネットのVLANの一覧を求める
1.3.6.1.4.1.9.9.46.1.3.1.1.4 VLANの名前
1.3.6.1.4.1.9.9.68.1.2.2.1.2 ポートに割り当てられているVLAN ID

SNMP自体の取り扱いに関しては、gemが公開されているのでそちらを使う。
あとはブラウザで見れるようにsinatraも使って書くとこんな感じ。

require 'bundler/setup'
require 'sinatra'
require 'snmp'

HOST          = '192.168.0.100'
VTP_VLAN_TYPE = '1.3.6.1.4.1.9.9.46.1.3.1.1.3'
VTP_VLAN_NAME = '1.3.6.1.4.1.9.9.46.1.3.1.1.4'
VM_VLAN       = '1.3.6.1.4.1.9.9.68.1.2.2.1.2'

get '/' do

  vlan_name_table = { }
  interfaces      = [ ]
  interface_names = { }

  SNMP::Manager.open(:host => HOST, :community => 'ouchi') do |manager|

    manager.walk([VTP_VLAN_TYPE, VTP_VLAN_NAME]) do |t, n|
      vlan_name_table[t.oid.to_s.split('.').last.to_i] = n.value.to_s if t.value.to_i == 1
    end

    manager.walk([VM_VLAN]) do |vb|
      interfaces.push({
        :index    => vb.first.oid.to_s.split('.').last.to_i,
        :assigned => vb.first.value.to_i
      })
    end

    manager.walk(['ifDescr']) do |vb|
      interface_names[vb.first.oid.to_s.split('.').last.to_i] = vb.first.value.to_s
    end

  end

  erb :index, :locals => {
    :vlan_name_table => vlan_name_table,
    :interfaces      => interfaces,
    :interface_names => interface_names
  }

end

helpers do
  def ifname(interface_name)
    interface_name.sub!(/FastEthernet/, 'fa ')
    interface_name.sub(/GigabitEthernet/, 'gi ')
  end
end

テンプレートはこう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
    <style>
      td, th {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="page-header">
        <h3>VLAN on <%= HOST %></h3>
      </div>
      <table class="table table-bordered">

        <thead>
          <tr>
            <th></th>
            <% interfaces.each do |i| %>
              <th><%= ifname(interface_names[i[:index]]) %></th>
            <% end %>
          </tr>
        </thead>

        <tbody>
          <% vlan_name_table.each do |vlan_id, vlan_name| %>
            <tr>
              <td><%= vlan_name %></td>
              <% interfaces.each do |i| %>
                <td>
                  <% if i[:assigned] == vlan_id %>
                    <span class="label label-info">assigned</span></td>
                  <% end %>
                </td>
              <% end %>
            </tr>
          <% end %>
        </tbody>

      </table>
    </div>
  </body>
</html>

ブラウザで見てみるとこうなる。

Webブラウザ上での表示

最近はSNMPよりもっと便利なNETCONFというプロトコルもあるそうなので、そういうのでもっと簡単に設定とかも自動化できるようになるといいね。