無計画にネットワークを構築すると、ケーブルの配線は悲惨なまでにごちゃごちゃになります。からみあったケーブルのせいで見通しが悪くなり、そのままさらにスイッチやサーバを増築していくと配線のやり直しとなります。こうなってしまう一番の原因は、スイッチとスイッチ、スイッチとサーバをケーブルで直接つないでしまうことです。これでは、つなぐものを増やせば増やすほどごちゃごちゃになっていくのは当然です。
これを解消するのがパッチパネルという装置です (図 6-1)。パッチパネルの仕組みはシンプルで、ケーブルを挿すためのコネクタがずらりと並んでいて、配線をいったんパッチパネルで中継できるようになっています。スイッチやサーバをいったん中継点となるパッチパネルにつなぎ、パッチパネル上の変更だけで全体の配線を自由に変更できるので、ケーブルがすっきりし拡張性も向上します。
パッチパネルをさらに便利にしたのが、いわゆるインテリジェント・パッチパネルです。インテリジェント・パッチパネルとは、パッチパネルをネットワーク経由で操作できるようにしたものです。従来のパッチパネルでは、メンテナンス性は向上できるとしても、配線を変更するたびにサーバ室まで足を運ぶという面倒さがありました。インテリジェント・パッチパネルを使えば、居室にいながらリモートでパッチパネルの配線を変更できるようになります。
インテリジェント・パッチパネルはOpenFlowで簡単に実装できます。パッチパネルでの中継のように、パケットを指定したコネクタからコネクタへ転送するというのは、フローエントリの代表的な使い方の一つだからです。
OpenFlowで実装したパッチパネルは図 6-2 のようになります。OpenFlowスイッチをパッチパネルに見立てて、接続を中継したいデバイス(ホストまたはスイッチ)をつなげます。コントローラはパケット転送のルールをフローエントリとしてOpenFlowスイッチに書き込むことで、仮想的なパッチを作ります。
たとえば図 6-2 のように、ポート1番と5番をつなげる場合を考えましょう。必要なフローエントリは次の2つです。
-
ポート1番に入力したパケットをポート5番に出力する
-
ポート5番に入力したパケットをポート1番に出力する
フローエントリの構成要素には、「こういうパケットが届いたとき」というマッチフィールドと、「こうする」というアクションがあるのでした。パッチパネルの場合、「ポートx番に入力」がマッチフィールドで、「ポートy番に出力」がアクションです。
それでは仕組みがわかったところで、パッチパネルコントローラを動かしてみましょう。
パッチパネルのソースコードはGitHubのtrema/patch_panelリポジトリ (https://github.com/trema/patch_panel) からダウンロードできます。
$ git clone https://github.com/trema/patch_panel.git
ダウンロードしたソースツリー上で bundle install --binstubs
を実行すると、Tremaなどの実行環境一式を自動的にインストールできます。
$ cd patch_panel $ bundle install --binstubs
以上でパッチパネルとTremaのセットアップは完了です。
パッチパネルのソースコードで主なファイルは次の 3 つです。
-
lib/patch_panel.rb
: パッチパネル本体 -
patch_panel.conf
: 仮想ネットワーク設定ファイル -
bin/patch_panel
: パッチパネルの操作コマンド
仮想ネットワーク設定ファイル patch_panel.conf
では、パッチパネルの動作テストのためにパケットを送受信できる仮想ホストを定義しています。vhost
で始まる行が仮想ホスト、そして link
で始まる行がスイッチやホストをつなげるための仮想リンクです。
link:vendor/patch_panel/patch_panel.conf[role=include]
この設定ファイルでは仮想スイッチ 0xabc
に 3 つの仮想ホスト host1
, host2
, host3
を接続しています (図 6-3)。仮想スイッチと仮想ホストの接続は、仮想リンク (link
で始まる行) によって記述できます。link
を書いた順で、それぞれのホストはスイッチのポート 1 番、ポート 2 番、ポート 3 番、… に接続されます。
パッチパネルをこの仮想ネットワーク内で実行するには、仮想ネットワーク設定ファイルを trema run
の -c
オプションに渡します。次のように trema run
コマンドでパッチパネルコントローラを起動してください。
$ ./bin/trema run ./lib/patch_panel.rb -c patch_panel.conf
パッチパネルは起動しただけではまだパッチングされていないので、ホスト間でのパケットは通りません。これを確認するために、trema send_packets
コマンドを使ってhost1とhost2の間でテストパケットを送ってみましょう。
$ ./bin/trema send_packets --source host1 --dest host2 $ ./bin/trema send_packets --source host2 --dest host1
正常に動いていれば、それぞれのホストでの受信パケット数は0になっているはずです。これを確認できるのが trema show_stats
コマンドです。
$ ./bin/trema show_stats host1 Packets sent: 192.168.0.1 -> 192.168.0.2 = 1 packet $ ./bin/trema show_stats host2 Packets sent: 192.168.0.2 -> 192.168.0.1 = 1 packet
trema show_stats
コマンドは引数として渡したホストの送受信パケットを表示します。host1 と host2 の両ホストともパケットを 1 つ送信していますが、どちらにもパケットは届いていません。
パッチパネルの設定は ./bin/patch_panel
コマンドで指定できます。たとえば、スイッチ 0xabc のポート 1 番とポート 2 番をつなぐには次のコマンドを実行します。
$ ./bin/patch_panel create 0xabc 1 2
これで、host1 と host2 が通信できるはずです。もういちどパケットの送受信を試してみましょう。
$ ./bin/trema send_packets --source host1 --dest host2 $ ./bin/trema send_packets --source host2 --dest host1 $ ./bin/trema show_stats host1 Packets sent: 192.168.0.1 -> 192.168.0.2 = 2 packets Packets received: 192.168.0.2 -> 192.168.0.1 = 1 packet $ ./bin/trema show_stats host2 Packets sent: 192.168.0.2 -> 192.168.0.1 = 2 packets Packets received: 192.168.0.1 -> 192.168.0.2 = 1 packet
たしかにパケットが届いています。パッチパネルの動作イメージがわかったところで、ソースコードを見ていきます。
パッチパネルのソースコードはlib/patch_panel.rbです。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
今までに学んだ知識で、このコードをできるだけ解読してみましょう。
-
パッチパネルの本体はPatchPanelという名前の小さなクラスである
-
このクラスには3 章「Hello, Trema!」で学んだ
switch_ready
ハンドラが定義してあり、この中でdelete_flow_entries
とadd_flow_entries
いうプライベートメソッドを呼んでいる。どうやらこれがパッチ処理の本体だ -
create_patch
とdelete_patch
というメソッドが定義してある。これらがパッチの作成と削除に対応していると予想できる -
add_flow_entries
メソッドではsend_flow_mod_add
を2回呼んでいる。1つのパッチを作るのに2つのフローエントリが必要なので、2回呼んでいるのだろうと推測できる
ここまでわかればしめたものです。あらかじめパッチパネルの仕組みを押さえていたので、ソースコードを読むのも簡単です。それでは、各部分のソースコードを詳しく見ていきましょう。
startハンドラではコントローラを初期化します。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
@patch
は現在のパッチング情報を入れておくハッシュテーブル (後述) です。このハッシュテーブルは、キーにスイッチの Datapath ID、バリューに現在のパッチ情報を持ちます。たとえば、スイッチ 0x1 のポート 1 番と 4 番をパッチングし、スイッチ 0x2 のポート 1 番と 2 番、および 3 番と 4 番をパッチングした場合、@patch
の中身は次のようになります。
Datapath ID (キー) |
パッチ情報 (バリュー) |
0x1 |
[[1, 4]] |
0x2 |
[[1, 2], [3, 4]] |
Note
|
Rubyのイディオム
Hash.new([].freeze)
hash = Hash.new([]) p hash[1] #=> [] p hash[1] << "bar" #=> ["bar"] p hash[1] #=> ["bar"] p hash[2] #=> ["bar"] #初期値が ["bar"] になってしまった そこで、初期値を hash = Hash.new([].freeze) hash[0] += [0] #破壊的でないメソッドはOK hash[1] << 1 # エラー `<<': can't modify frozen array (TypeError) |
ハッシュテーブルは中カッコで囲まれた ({}
) 辞書です。辞書とは「言葉をその定義に対応させたデータベース」です。Rubyでは、この対応を :
という記号で次のように表します。
animals = { armadillo: 'アルマジロ', boar: 'イノシシ' }
たとえば ”boar” を日本語で言うと何だろう? と辞書で調べたくなったら、次のようにして辞書を引きます。
animals[:boar] #=> "イノシシ"
この辞書を引くときに使う言葉 (この場合は :boar
) をキーと言います。そして、見つかった定義 (この場合はイノシシ) をバリューと言います。
新しい動物を辞書に加えるのも簡単です。
animals[:cow] = 'ウシ'
Rubyのハッシュテーブルはとても高機能なので、文字列だけでなく好きなオブジェクトを格納できます。たとえば、パッチパネルでは Datapath ID をキーとして、パッチング情報 (配列) をバリューにします。
@patch[0x1] = [[1, 2], [3, 4]]
実は、すでにいろんなところでハッシュテーブルを使ってきました。たとえば、send_flow_mod_add
などの省略可能なオプションは、コロン (:
) を使っていることからもわかるように実はハッシュテーブルなのです。Rubyでは、引数の最後がハッシュテーブルである場合、その中カッコを次のように省略できます。
def flow_mod(message, port_no)
send_flow_mod_add(
message.datapath_id,
match: ExactMatch.new(message),
actions: SendOutPort.new(port_no)
)
end
# これと同じ
def flow_mod(message, port_no)
send_flow_mod_add(
message.datapath_id,
{ match: ExactMatch.new(message),
actions: SendOutPort.new(port_no) }
)
end
switch_ready
ハンドラは、起動してきたスイッチに対してパッチング用のフローエントリを書き込みます。すでにパッチ情報 @patch
にフローエントリ情報が入っていた場合(スイッチがいったん停止して再接続した場合など)のみ、フローエントリを入れ直します。
def switch_ready(dpid)
@patch[dpid].each do |port_a, port_b| # (1)
delete_flow_entries dpid, port_a, port_b # (2)
add_flow_entries dpid, port_a, port_b # (3)
end
end
-
@patch[dpid].each
はパッチング設定を 1 つずつ処理するイテレータ (後述)。仮引数はport_a
とport_b
の2つで、それぞれにパッチでつなぐポート番号が 1 つずつ入る -
プライベートメソッド
delete_flow_entries
は古いフローエントリを消す。 -
プライベートメソッド
add_flow_entries
がパッチング追加処理の本体。起動してきたスイッチのDatapath ID、およびパッチングするポート番号2つを引数に取る
イテレータとは繰り返すものという意味で、繰り返し処理を短く書きたいときに使います。イテレータは一般に次の形をしています。
fruits = ["バナナ", "みかん", "りんご"]
fruits.each do |each|
puts each
end
実行結果:
バナナ
みかん
りんご
ここでは配列 fruits
の each
というイテレータで fruits
の各要素をプリントアウトしています。do
のあとにある each
は fruits の各要素が入る仮引数です。この each
にバナナ・みかん・りんごが順にセットされ、続くブロック (do…end
) が呼び出されます。
イテレータの利点は、同じ繰り返し処理をループで書いた場合よりもずっと簡単に書けることです。
for (int i = 0; i < 3; i++) { puts fruits[i]; }
このように for ループで書くと、配列の要素にアクセスするための変数 i
やループの終了条件 i < 3
などが必要になります。一方、イテレータはこうした煩雑なものを抽象化で見えなくしてくれるので、プログラマは各要素についてやりたいことだけをブロックに書けば動きます。
実は、5 章「マイクロベンチマークCbench」で登場した次のコードもイテレータです。
# start_worker_thread メソッドを 10 回実行
10.times { start_worker_thread }
# または、下のようにも書ける
# n には 1〜10 が入る。ただしここでは n は使っていない
10.times { |n| start_worker_thread }
この times
がイテレータで、続くブロックの内容を 10 回実行します。もし仮引数を使わない場合は書かなくても OK です。これによって、「10.times { start_worker_thread }
(10 回 start_worker_thread を呼ぶ)」といったふうに繰り返しを非常に直感的に書けます。
1つのパッチ(2つのフローエントリ)を実際に書き込むのが add_flow_entries
プライベートメソッドです。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
add_flow_entries
の中で2回呼び出している send_flow_mod_add
のうち、最初の呼び出し部分を詳しく見てみましょう。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
ここでは、ポート port_a
番へ上がってきたパケットをポート port_b
番へ出力するフローエントリを書き込んでいます。ここでは次の2つのオプションを指定しています。
-
match
: 「入力ポート(:in_port
)がport_a
であった場合」というMatch
オブジェクト -
actions
: 「ポートport_b
番へ出力する」というSendOutPort
アクション
delete_flow_entries
は古いフローエントリを消すメソッドです。add_flow_entries
でフローエントリを足す前に、いったん delete_flow_entries
で古いフローエントリを消すことでフローエントリが重複しないようにします。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
ここで呼び出している send_flow_mod_delete
は send_flow_mod_add
とは逆のメソッドで、match:
に対応するフローエントリを削除します。
create_patch
と delete_patch
メソッドは、bin/patch_panel
コマンドからパッチの作成と削除を行うためのAPIです。
create_patch
メソッドは、add_flow_entries
メソッドでフローエントリを追加し、パッチ設定 @patch
にパッチ情報を追加します。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
逆に delete_patch
メソッドはフローエントリを削除しパッチ設定からパッチ情報を削除します。
link:vendor/patch_panel/lib/patch_panel.rb[role=include]
PatchPanel
クラスの操作コマンドが bin/patch_panel
です。PatchPanel
クラスの create_patch
と delete_patch
メソッドを呼び出します。patch_panel create
と patch_panel delete
という 2 つのサブコマンドを持っています。
サブコマンドの実装には gli (https://github.com/davetron5000/gli) というRubyライブラリを使っています。gli を使うと、patch_panel create
や patch_panel delete
といったサブコマンド体系、いわゆるコマンドスイートを簡単に実装できます。詳細は gli のドキュメントにゆずりますが、書きかたを簡単に紹介しておきます。
#!/usr/bin/env ruby
require 'rubygems'
require 'bundler'
Bundler.setup :default
require 'gli'
require 'trema'
# patch_panel command
module PatchPanelApp
extend GLI::App
desc 'Creates a new patch' # (1)
arg_name 'dpid port#1 port#2'
command :create do |c|
c.desc 'Location to find socket files'
c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR
c.action do |_global_options, options, args|
dpid = args[0].hex
port1 = args[1].to_i
port2 = args[2].to_i
Trema.trema_process('PatchPanel', options[:socket_dir]).controller.
create_patch(dpid, port1, port2)
end
end
desc 'Deletes a patch' # (2)
arg_name 'dpid port#1 port#2'
command :delete do |c|
c.desc 'Location to find socket files'
c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR
c.action do |_global_options, options, args|
dpid = args[0].hex
port1 = args[1].to_i
port2 = args[2].to_i
Trema.trema_process('PatchPanel', options[:socket_dir]).controller.
delete_patch(dpid, port1, port2)
end
end
exit run(ARGV)
end
-
create
サブコマンドの実装 -
delete
サブコマンドの実装
gli を使ったサブコマンドの実装は、command サブコマンド名 do … end
のブロックを記述するだけです。それぞれのブロック内で、サブコマンドに渡されたオプションの処理と実際の動作を記述します。
desc 'Creates a new patch' # (1)
arg_name 'dpid port#1 port#2' # (2)
command :create do |c|
c.desc 'Location to find socket files' # (3)
c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR # (4)
c.action do |_global_options, options, args|
dpid = args[0].hex # (5)
port1 = args[1].to_i # (5)
port2 = args[2].to_i # (5)
Trema.trema_process('PatchPanel', options[:socket_dir]).controller.
create_patch(dpid, port1, port2) # (6)
end
end
-
create
サブコマンドの説明 -
オプションの説明
-
-S
(--socket_dir
) オプションの説明 -
-S
(--socket_dir
) オプションとデフォルト値の定義 -
オプションのパース
-
PatchPanel
クラスのcreate_patch
メソッドの呼び出し
ポイントは、サブコマンド定義内での PatchPanel
クラスのメソッド呼び出し部分です。
Trema.trema_process('PatchPanel', options[:socket_dir]).controller.create_patch(dpid, port1, port2)
この Trema.trema_process.controller
メソッドは、現在動いているコントローラオブジェクト(PatchPanel
クラスオブジェクト)を返します。そしてその返り値に対して create_patch
などのメソッドを呼び出すことで、コントローラのメソッドを呼び出せます。
フローを使ってパケットを転送する方法の入門編として、OpenFlowで実現するインテリジェント・パッチパネルを書きました。
-
仮想スイッチに仮想ホストを接続してテストパケットを送信する方法を学んだ
-
フローエントリの削除方法を学んだ
-
コントローラ操作用の外部コマンドの書き方を学んだ
実は、今回作ったOpenFlow版パッチパネルはSDNの一種です。なぜならば、OpenFlow版パッチパネルを使えばホストの所属するネットワークをソフトウェア的に切り替えられるからです。これは、物理ネットワークの上にそれぞれ独立したネットワークをいくつも作れるという意味で、もっとも単純なネットワーク仮想化に他なりません。
続く章では、これまで使ってきた 3 つの重要な OpenFlow メッセージである Flow Mod, Packet In, Packet Out を組み合わせてイーサネットスイッチ作りに挑戦です。