OpenFlowではたくさんのスイッチを1つのコントローラで集中制御できます。スイッチにはフローテーブルに従ったパケットの転送という1つの仕事だけをやらせ、頭脳であるコントローラが全体のフローテーブルを統括するというわけです。これによって1 章「OpenFlow の仕組み」で見てきたように、自動化やさまざまなシステム連携・トラフィック制御のしやすさ・ソフトウェア開発のテクニック適用・水平方向へのアップグレード、といったさまざまなメリットが生まれるのでした。
本章ではこの集中制御の一例として、スイッチ監視ツールを作ります。このツールは「今、ネットワーク中にどんなスイッチが動いていて、それぞれがどんな状態か」をリアルタイムに表示します。OpenFlowでの集中制御に必要な基本テクニックをすべて含んでいます。
スイッチ監視ツールは図 4-1のように動作します。コントローラはスイッチの接続を検知すると、起動したスイッチの情報を表示します。逆にスイッチが予期せぬ障害など何らかの原因で接続を切った場合、コントローラはこれを検知して警告を表示します。
スイッチ監視ツールのソースコードは GitHub から次のようにダウンロードできます。
$ git clone https://github.com/trema/switch_monitor.git
ダウンロードしたソースツリー上で bundle install --binstubs
を実行すると、Tremaの ./bin/trema
コマンドなど必要な実行環境一式を自動的にインストールできます。
$ cd switch_monitor $ bundle install --binstubs
以上でスイッチ監視ツールとTremaのセットアップは完了です。
試しに仮想スイッチ3台の構成でスイッチ監視ツールを起動してみましょう。次の内容の設定ファイルを switch_monitor.conf
として保存してください。なお、それぞれの datapath_id
がかぶらないように 0x1
, 0x2
, 0x3
と連番を振っていることに注意してください。
link:vendor/switch_monitor/trema.conf[role=include]
この構成でスイッチ監視ツールを起動するには、この設定ファイルを trema run
の -c
オプションに渡すのでした。スイッチ監視ツールの出力は次のようになります。
$ ./bin/trema run ./lib/switch_monitor.rb -c switch_monitor.conf
SwitchMonitor started.
All =
0x3 is up (All = 0x3) # (1)
0x3 manufacturer = Nicira, Inc. # (2)
0x3 hardware info = # (3)
0x3 software info = # (4)
0x3 serial number = # (5)
0x3 description = # (6)
0x1 is up (All = 0x1, 0x3)
0x1 manufacturer = Nicira, Inc.
0x1 hardware info =
0x1 software info =
0x1 serial number =
0x1 description =
0x2 is up (All = 0x1, 0x2, 0x3)
0x2 manufacturer = Nicira, Inc.
0x2 hardware info =
0x2 software info =
0x2 serial number =
0x2 description =
All = 0x1, 0x2, 0x3
All = 0x1, 0x2, 0x3
-
スイッチ 0x3 がコントローラに接続
-
スイッチの製造者情報
-
スイッチのハードウェア情報 (空)
-
スイッチのソフトウェア情報 (空)
-
スイッチのシリアル番号 (空)
-
スイッチの詳細情報 (空)
0x1 is up
などの行から、仮想ネットワーク設定ファイルに定義したスイッチ3台をコントローラが検出していることがわかります。続く行では、スイッチの製造者といった詳細情報や、スイッチ一覧 (All = 0x1, 0x2, 0x3
の行) も確認できます。
このように実際にスイッチを持っていなくても、設定ファイルを書くだけでスイッチを何台も使ったコントローラの動作テストができます。設定ファイルの vswitch { … }
の行を増やせば、スイッチをさらに5台、10台、…と足していくことも思いのままです。
それでは、スイッチの切断をうまく検出できるか確かめてみましょう。仮想スイッチを停止するコマンドは trema stop
です。trema run
を実行したターミナルはそのままで別ターミナルを開き、次のコマンドで仮想スイッチ 0x3
を落としてみてください。
$ ./bin/trema stop 0x3
すると、trema run
を実行したターミナルで新たに 0x3 is down
の行が出力されます。
$ ./bin/trema run ./switch_monitor.rb -c ./switch_monitor.conf
SwitchMonitor started.
All =
0x3 is up (All = 0x3)
0x3 manufacturer = Nicira, Inc.
0x3 hardware info =
0x3 software info =
0x3 serial number =
0x3 description =
……
All = 0x1, 0x2, 0x3
All = 0x1, 0x2, 0x3
All = 0x1, 0x2, 0x3
0x3 is down (All = 0x1, 0x2) # (1)
-
スイッチ 0x3 が停止したことを示すログメッセージ
うまくいきました! それでは逆に、さきほど落した仮想スイッチを再び起動してみましょう。仮想スイッチを起動するコマンドは trema start
です。
$ ./bin/trema start 0x3
0x3 is up
の行が出力されれば成功です。
$ ./bin/trema run ./switch_monitor.rb -c ./switch_monitor.conf
SwitchMonitor started.
All =
0x3 is up (All = 0x3)
0x3 manufacturer = Nicira, Inc.
0x3 hardware info =
0x3 software info =
0x3 serial number =
0x3 description =
……
All = 0x1, 0x2, 0x3
All = 0x1, 0x2, 0x3
0x3 is down (All = 0x1, 0x2)
All = 0x1, 0x2
……
All = 0x1, 0x2
All = 0x1, 0x2
0x3 is up (All = 0x1, 0x2, 0x3) # (1)
-
スイッチ 0x3 が再び起動したことを示すログメッセージ
このように、trema stop
と trema start
は仮想ネットワークのスイッチを制御するためのコマンドです。引数にスイッチのDatapath IDを指定することで、スイッチを停止または起動してコントローラの反応を確かめられます。
- trema stop [Datapath ID]
-
指定した仮想スイッチを停止する
- trema start [Datapath ID]
-
指定した仮想スイッチを再び起動する
スイッチ監視ツールの動作イメージがわかったところで、そろそろソースコードの解説に移りましょう。
まずはざっとスイッチ監視ツールのソースコード(lib/switch_monitor.rb)を眺めてみましょう。今までに学んできたRubyの品詞を頭の片隅に置きながら、次のコードに目を通してみてください。
# Switch liveness monitor.
class SwitchMonitor < Trema::Controller
timer_event :show_all_switches, interval: 10.sec
def start(_args)
@switches = []
logger.info "#{name} started."
end
def switch_ready(dpid)
@switches << dpid
logger.info "#{dpid.to_hex} is up (All = #{all_switches_in_string})"
send_message dpid, DescriptionStats::Request.new
end
def switch_disconnected(dpid)
@switches -= [dpid]
logger.info "#{dpid.to_hex} is down (All = #{all_switches_in_string})"
end
def description_stats_reply(dpid, desc)
logger.info "Switch #{dpid.to_hex} manufacturer = #{desc.manufacturer}"
logger.info "Switch #{dpid.to_hex} hardware info = #{desc.hardware}"
logger.info "Switch #{dpid.to_hex} software info = #{desc.software}"
logger.info "Switch #{dpid.to_hex} serial number = #{desc.serial_number}"
logger.info "Switch #{dpid.to_hex} description = #{desc.datapath}"
end
private
def show_all_switches
logger.info "All = #{all_switches_in_string}"
end
def all_switches_in_string
@switches.sort.map(&:to_hex).join(', ')
end
end
新しい品詞や構文がいくつかありますが、今までに学んだ知識だけでこのRubyソースコードの構成はなんとなくわかったはずです。まず、スイッチ監視ツールの本体は SwitchMonitor
という名前のクラスです。そしてこのクラスにはいくつかハンドラメソッドが定義してあるようです。おそらくそれぞれがスイッチの接続や切断、そして統計情報イベントを処理しているんだろう、ということが想像できれば上出来です。
switch_ready
ハンドラでは、スイッチ一覧リスト @switches
に新しく接続したスイッチのDatapath IDを追加し、接続したスイッチの情報を画面に表示します。
link:vendor/switch_monitor/lib/switch_monitor.rb[role=include]
@switches
は start
ハンドラで空の配列に初期化されます。
def start(_args)
@switches = []
logger.info "#{name} started."
end
アットマーク(@
)で始まる語はインスタンス変数です。インスタンス変数はたとえば人間の歳や身長などといった、属性を定義するときによく使われます。アットマークはアトリビュート (属性) を意味すると考えれば覚えやすいでしょう。
インスタンス変数は同じクラスの中のメソッド定義内であればどこからでも使えます。具体的な例として次の Human
クラスを見てください。
class Human
def initialize
@age = 0 # (1)
end
def birthday # (2)
@age += 1
end
end
-
インスタンス変数を初期化。生まれたときは 0 歳
-
一年に一度、歳をとる
Human
クラスで定義される Human
オブジェクトは、初期化したときにはそのインスタンス変数 @age
は0、つまり0歳です。birthday
を呼び出すたびに歳を取り、@age
が 1 増えます。このように @age
は initialize
および birthday
メソッドのどちらからでもその値を変更できます。
配列は角カッコで囲まれたリストで、カンマで区切られています。
-
[]
は空の配列 -
[1, 2, 3]
は数字の配列 -
["バナナ", "みかん", "りんご"]
は文字列の配列
Rubyの配列はとても直感的に要素を足したり取り除いたりできます。たとえば配列の最後に要素を加えるには <<
を使います。
fruits = ["バナナ", "みかん", "りんご"]
fruits << "パイナップル"
#=> ["バナナ", "みかん", "りんご", "パイナップル"]
配列から要素を取り除くには -=
を使います。これは左右の配列同士を見比べ、共通する要素を取り除いてくれます。
fruits = ["バナナ", "みかん", "テレビ", "りんご", "たわし"]
fruits -= ["テレビ", "たわし"]
#=> ["バナナ", "みかん", "りんご"]
配列はRubyで多用するデータ構造で、この他にもたくさんのメソッドがあらかじめ定義されています。もし詳しく知りたい人は3 章「Hello, Trema!」の参考文献で紹介したRubyのサイトや書籍を参照してください。
switch_disconnected
ハンドラでは、スイッチ一覧リストから切断したスイッチのDatapath IDを削除し、切断したスイッチの情報を画面に表示します。
link:vendor/switch_monitor/lib/switch_monitor.rb[role=include]
ここでは switch_ready
とは逆に、配列の引き算 (-=
) で切断したスイッチのDatapath IDを @switches
から除いていることに注意してください。
スイッチの一覧を一定時間ごとに表示するには、Tremaのタイマー機能を使います。次のように timer_event
に続いて一定間隔ごとに呼び出したいメソッドと呼び出し間隔を指定しておくと、指定したメソッドが指定した間隔ごとに呼ばれます。
# 1 年に一度、年をとるクラス
class Human < Trema::Controller
timer_event :birthday, interval: 1.year # (1)
...
private # (2)
def birthday # (3)
@age += 1
end
-
1 年ごとに
birthday
メソッドを呼ぶ -
この行から下はプライベートメソッド
-
タイマーから呼ばれる
birthday
メソッド
この定義は Human
クラス定義の先頭に書けるので、まるで Human
クラスの属性としてタイマーをセットしているように読めます。このようにTremaを使うとタイマー処理も短く読みやすく書けます。
タイマーから呼び出すメソッドは、クラスの中だけで使うのでよくプライベートなメソッドとして定義します。Rubyでは private
と書いた行以降のメソッドはプライベートメソッドとして定義され、クラスの外からは見えなくなります。
これを踏まえてスイッチ監視ツールのソースコードのタイマー部分を見てみましょう。
class SwitchMonitor < Trema::Controller
timer_event :show_all_switches, interval: 10.sec
...
private
def show_all_switches
logger.info "All = #{all_switches_in_string}"
end
クラス名定義直後のタイマー定義より、10秒ごとに show_all_switches
メソッドを呼んでいることがわかります。
シンボルは文字列の軽量版と言える品詞です。:a
・:number
・:show_all_switches
のように必ずコロンで始まり、英字・数字・アンダースコアを含みます。シンボルは定数のように一度決めると変更できないので、文字列のようにいつの間にか書き変わっている心配がありません。このため、ハッシュテーブル (6 章「インテリジェントなパッチパネル」参照) の検索キーとしてよく使われます。
また、シンボルは誰かにメソッドを名前で渡すときにも登場します。これだけですとわかりづらいと思うので、具体的な例を見ていきましょう。リスト switch_monitor.rb
には、次のようにシンボルを使っている箇所がありました。
link:vendor/switch_monitor/lib/switch_monitor.rb[role=include]
この :show_all_switches
は SwitchMonitor
クラスのメソッド名をシンボルで書いたものです。
もしここでシンボルを使わずに、直接次のように指定するとどうなるでしょうか。
# まちがい!
timer_event show_all_switches, interval: 10.sec
これではうまく動きません。なぜならば、ソースコードの中に show_all_switches
とメソッドの名前を書いた時点でそのメソッドが実行されてしまい、その返り値が timer_event
へと渡されてしまうからです。
もしメソッド名を何かに渡すときにはかならずシンボルにする、と覚えましょう。
スイッチの情報を取得するには、取得したい情報をリクエストするメッセージを send_message
でスイッチに送信し、そのリプライメッセージをハンドラで受け取ります。たとえば、今回のようにスイッチの詳細情報を取得するには、DescriptionStats::Request
メッセージを送信し、対応するハンドラ description_stats_reply
でメッセージを受け取ります。
def switch_ready(dpid)
@switches << dpid
logger.info "#{dpid.to_hex} is up (All = #{all_switches_in_string})"
send_message dpid, DescriptionStats::Request.new
end
def description_stats_reply(dpid, desc)
logger.info "Switch #{dpid.to_hex} manufacturer = #{desc.manufacturer}"
logger.info "Switch #{dpid.to_hex} hardware info = #{desc.hardware}"
logger.info "Switch #{dpid.to_hex} software info = #{desc.software}"
logger.info "Switch #{dpid.to_hex} serial number = #{desc.serial_number}"
logger.info "Switch #{dpid.to_hex} description = #{desc.datapath}"
end
スイッチの詳細情報のほかにも、さまざまな統計情報を取得できます。OpenFlow 1.0がサポートしている統計情報の一覧は次のとおりです。
取得できる情報 | スイッチへ送るメッセージ | ハンドラ名 |
---|---|---|
スイッチの詳細情報 |
|
|
単一フローエントリの統計情報 |
|
|
複数フローエントリの統計情報 |
|
|
フローテーブルの統計情報 |
|
|
スイッチポートの統計情報 |
|
|
キューの統計情報 |
|
|
この章ではスイッチの動作状況を監視するスイッチ監視ツールを作りました。また、作ったスイッチ監視ツールをテストするため Trema の仮想ネットワーク機能を使いました。
-
スイッチの起動と切断を捕捉するには、
switch_ready
とswitch_disconnected
ハンドラメソッドを定義する -
スイッチの詳細情報を取得するには、
DescriptionStats::Request
メッセージをスイッチへ送信しdescription_stats_reply
ハンドラでリプライを受信する -
タイマー (
timer_event
) を使うと一定間隔ごとに指定したメソッドを起動できる -
trema start
とtrema stop
コマンドで仮想ネットワーク内のスイッチを起動/停止できる
続く章では、いよいよ OpenFlow の最重要メッセージである Packet In と Flow Mod を使ったプログラミングに挑戦です。