Skip to content

Latest commit

 

History

History
473 lines (346 loc) · 23.9 KB

patch_panel.adoc

File metadata and controls

473 lines (346 loc) · 23.9 KB

インテリジェントなパッチパネル

日々のネットワーク管理に役立ち、さらにネットワーク仮想化の入門にもなるのがこのOpenFlowで作るパッチパネルです。そのうえソースコードも簡単とくれば、試さない手はありません。

cables

便利なインテリジェント・パッチパネル

無計画にネットワークを構築すると、ケーブルの配線は悲惨なまでにごちゃごちゃになります。からみあったケーブルのせいで見通しが悪くなり、そのままさらにスイッチやサーバを増築していくと配線のやり直しとなります。こうなってしまう一番の原因は、スイッチとスイッチ、スイッチとサーバをケーブルで直接つないでしまうことです。これでは、つなぐものを増やせば増やすほどごちゃごちゃになっていくのは当然です。

これを解消するのがパッチパネルという装置です (図 6-1)。パッチパネルの仕組みはシンプルで、ケーブルを挿すためのコネクタがずらりと並んでいて、配線をいったんパッチパネルで中継できるようになっています。スイッチやサーバをいったん中継点となるパッチパネルにつなぎ、パッチパネル上の変更だけで全体の配線を自由に変更できるので、ケーブルがすっきりし拡張性も向上します。

patch panel
図 6-1: ごちゃごちゃした配線をパッチパネルですっきりと

パッチパネルをさらに便利にしたのが、いわゆるインテリジェント・パッチパネルです。インテリジェント・パッチパネルとは、パッチパネルをネットワーク経由で操作できるようにしたものです。従来のパッチパネルでは、メンテナンス性は向上できるとしても、配線を変更するたびにサーバ室まで足を運ぶという面倒さがありました。インテリジェント・パッチパネルを使えば、居室にいながらリモートでパッチパネルの配線を変更できるようになります。

OpenFlow版インテリジェント・パッチパネル

インテリジェント・パッチパネルはOpenFlowで簡単に実装できます。パッチパネルでの中継のように、パケットを指定したコネクタからコネクタへ転送するというのは、フローエントリの代表的な使い方の一つだからです。

OpenFlowで実装したパッチパネルは図 6-2 のようになります。OpenFlowスイッチをパッチパネルに見立てて、接続を中継したいデバイス(ホストまたはスイッチ)をつなげます。コントローラはパケット転送のルールをフローエントリとしてOpenFlowスイッチに書き込むことで、仮想的なパッチを作ります。

openflow patch panel
図 6-2: 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 で始まる行がスイッチやホストをつなげるための仮想リンクです。

patch_panel.conf
link:vendor/patch_panel/patch_panel.conf[role=include]

この設定ファイルでは仮想スイッチ 0xabc に 3 つの仮想ホスト host1, host2, host3 を接続しています (図 6-3)。仮想スイッチと仮想ホストの接続は、仮想リンク (link で始まる行) によって記述できます。link を書いた順で、それぞれのホストはスイッチのポート 1 番、ポート 2 番、ポート 3 番、…​ に接続されます。

configuration
図 6-3設定ファイル patch_panel.conf の仮想ネットワーク構成

パッチパネルをこの仮想ネットワーク内で実行するには、仮想ネットワーク設定ファイルを 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です。

lib/patch_panel.rb
link:vendor/patch_panel/lib/patch_panel.rb[role=include]

今までに学んだ知識で、このコードをできるだけ解読してみましょう。

  • パッチパネルの本体はPatchPanelという名前の小さなクラスである

  • このクラスには3 章「Hello, Trema!」で学んだ switch_ready ハンドラが定義してあり、この中で delete_flow_entriesadd_flow_entries いうプライベートメソッドを呼んでいる。どうやらこれがパッチ処理の本体だ

  • create_patchdelete_patch というメソッドが定義してある。これらがパッチの作成と削除に対応していると予想できる

  • add_flow_entries メソッドでは send_flow_mod_add を2回呼んでいる。1つのパッチを作るのに2つのフローエントリが必要なので、2回呼んでいるのだろうと推測できる

ここまでわかればしめたものです。あらかじめパッチパネルの仕組みを押さえていたので、ソースコードを読むのも簡単です。それでは、各部分のソースコードを詳しく見ていきましょう。

startハンドラ

startハンドラではコントローラを初期化します。

PatchPanel#start (lib/patch_panel.rb)
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.new の引数 (バリューの初期値) である [].freeze はハッシュテーブルの初期値が変わらないようにするための Ruby のイディオムです。もしも、.freeze していない初期値 [] に対して << などの破壊的な操作をすると、次のように初期値が壊れてしまいます。

hash = Hash.new([])

p hash[1]          #=> []
p hash[1] << "bar" #=> ["bar"]
p hash[1]          #=> ["bar"]

p hash[2]          #=> ["bar"] #初期値が ["bar"] になってしまった

そこで、初期値を .freeze することで破壊的操作を禁止できます。

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ハンドラ

switch_ready ハンドラは、起動してきたスイッチに対してパッチング用のフローエントリを書き込みます。すでにパッチ情報 @patch にフローエントリ情報が入っていた場合(スイッチがいったん停止して再接続した場合など)のみ、フローエントリを入れ直します。

PatchPanel#switch_ready (lib/patch_panel.rb)
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
  1. @patch[dpid].each はパッチング設定を 1 つずつ処理するイテレータ (後述)。仮引数は port_aport_b の2つで、それぞれにパッチでつなぐポート番号が 1 つずつ入る

  2. プライベートメソッド delete_flow_entries は古いフローエントリを消す。

  3. プライベートメソッド add_flow_entries がパッチング追加処理の本体。起動してきたスイッチのDatapath ID、およびパッチングするポート番号2つを引数に取る

イテレータ

イテレータとは繰り返すものという意味で、繰り返し処理を短く書きたいときに使います。イテレータは一般に次の形をしています。

よくあるイテレータの使用例
fruits = ["バナナ", "みかん", "りんご"]

fruits.each do |each|
  puts each
end

実行結果:
バナナ
みかん
りんご

ここでは配列 fruitseach というイテレータで fruits の各要素をプリントアウトしています。do のあとにある each は fruits の各要素が入る仮引数です。この each にバナナ・みかん・りんごが順にセットされ、続くブロック (do…​end) が呼び出されます。

イテレータの利点は、同じ繰り返し処理をループで書いた場合よりもずっと簡単に書けることです。

C 言語っぽい for ループで書いた場合
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 を呼ぶ)」といったふうに繰り返しを非常に直感的に書けます。

add_flow_entriesメソッド

1つのパッチ(2つのフローエントリ)を実際に書き込むのが add_flow_entries プライベートメソッドです。

PatchPanel#add_flow_entries (lib/patch_panel.rb)
link:vendor/patch_panel/lib/patch_panel.rb[role=include]

add_flow_entries の中で2回呼び出している send_flow_mod_add のうち、最初の呼び出し部分を詳しく見てみましょう。

PatchPanel#add_flow_entries (lib/patch_panel.rb)
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メソッド

delete_flow_entries は古いフローエントリを消すメソッドです。add_flow_entries でフローエントリを足す前に、いったん delete_flow_entries で古いフローエントリを消すことでフローエントリが重複しないようにします。

PatchPanel#delete_flow_entries (lib/patch_panel.rb)
link:vendor/patch_panel/lib/patch_panel.rb[role=include]

ここで呼び出している send_flow_mod_deletesend_flow_mod_add とは逆のメソッドで、match: に対応するフローエントリを削除します。

create_patch, delete_patchメソッド

create_patchdelete_patch メソッドは、bin/patch_panel コマンドからパッチの作成と削除を行うためのAPIです。

create_patch メソッドは、add_flow_entries メソッドでフローエントリを追加し、パッチ設定 @patch にパッチ情報を追加します。

PatchPanel#create_patch (lib/patch_panel.rb)
link:vendor/patch_panel/lib/patch_panel.rb[role=include]

逆に delete_patch メソッドはフローエントリを削除しパッチ設定からパッチ情報を削除します。

PatchPanel#delete_patch (lib/patch_panel.rb)
link:vendor/patch_panel/lib/patch_panel.rb[role=include]

bin/patch_panel コマンド

PatchPanel クラスの操作コマンドが bin/patch_panel です。PatchPanel クラスの create_patchdelete_patch メソッドを呼び出します。patch_panel createpatch_panel delete という 2 つのサブコマンドを持っています。

サブコマンドの実装には gli (https://github.com/davetron5000/gli) というRubyライブラリを使っています。gli を使うと、patch_panel createpatch_panel delete といったサブコマンド体系、いわゆるコマンドスイートを簡単に実装できます。詳細は gli のドキュメントにゆずりますが、書きかたを簡単に紹介しておきます。

bin/patch_panel
#!/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
  1. create サブコマンドの実装

  2. delete サブコマンドの実装

gli を使ったサブコマンドの実装は、command サブコマンド名 do …​ end のブロックを記述するだけです。それぞれのブロック内で、サブコマンドに渡されたオプションの処理と実際の動作を記述します。

create サブコマンドの実装
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
  1. create サブコマンドの説明

  2. オプションの説明

  3. -S (--socket_dir) オプションの説明

  4. -S (--socket_dir) オプションとデフォルト値の定義

  5. オプションのパース

  6. 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 を組み合わせてイーサネットスイッチ作りに挑戦です。