2013年10月22日火曜日

Tremaを試す ~traffic monitor その1~

こんにちは。鯵王です。

前回のlearning_switchはわかったでしょうか?
learning_switchがわかっていれば、今回のtraffic_monitorもわかると思います。

ではさっそくtraffic_monitorのソースを見て行きましょう。

(traffic-monitor.rb)
 1  require "counter"
 2  require "fdb"
 3  
 4  class TrafficMonitor < Controller
 5    periodic_timer_event :show_counter, 10
 6  
 7    def start
 8      @counter = Counter.new
 9      @fdb = FDB.new
10    end
11  
12    def packet_in datapath_id, message
13      macsa = message.macsa
14      macda = message.macda
15  
16      @fdb.learn macsa, message.in_port
17      @counter.add macsa, 1, message.total_len
18      out_port = @fdb.lookup( macda )
19      if out_port
20        packet_out datapath_id, message, out_port
21        flow_mod datapath_id, macsa, macda, out_port
22      else
23        flood datapath_id, message
24      end
25    end
26  
27    def flow_removed datapath_id, message
28      @counter.add message.match.dl_src, message.packet_count, message.byte_count
29    end
30  
31    private
32  
33    def show_counter
34      puts Time.now
35      @counter.each_pair do | mac, counter |
36        puts "#{ mac } #{ counter[ :packet_count ] } packets (#{ counter[ :byte_count ] } bytes)"
37      end
38    end
39  
40    def flow_mod datapath_id, macsa, macda, out_port
41      send_flow_mod_add(
42        datapath_id,
43        :hard_timeout => 10,
44        :match => Match.new( :dl_src => macsa, :dl_dst => macda ),
45        :actions => ActionOutput.new( out_port )
46      )
47    end
48  
49    def packet_out datapath_id, message, out_port
50      send_packet_out(
51        datapath_id,
52        :packet_in => message,
53        :actions => ActionOutput.new( out_port )
54      )
55    end
56  
57    def flood datapath_id, message
58      packet_out datapath_id, message, OFPP_FLOOD
59    end


このプログラムではlearning switchをベースに以下のような変更をしています。
・パケットの送信回数、送信バイト数を集計し、定期的に画面に表示します
・フローエントリの有効期限を10秒に設定し、フローエントリを削除します
・フローエントリが削除されるタイミングでスイッチから送信回数と送信バイト数を取得します

1~2行目
requireではCounterクラスとFDBクラスを読み込んでいます。
FDBクラスは前回のlearning_switchに出てきたものよりもだいぶシンプルな内容になっています。
Counterクラスはパケットの送信回数と送信バイト数を管理するクラスです。
これらのクラスの説明は後ほど行います。

4行目
TrafficMonitorクラスを定義しています。

5行目
periodic_timer_eventはタイマーでメソッドを実行します。
「show_counter」メソッドを10秒おきに実行します。

6~10行目
startメソッドでCounterクラスのオブジェクトとFDBクラスのオブジェクトを作成しています。
@counterと@fdbはTrafficMonitorクラスの内で参照できます。

12~14行目
packet_inイベントハンドラです。
スイッチのフローテーブルにマッチしないパケットを受け取った時に実行されます。
引数のmessageから送信元のmacアドレスと送信先のmacアドレスを取得し、変数にセットしています。

16行目
@fdb.learnはFDBクラスのメソッドです。
このメソッドはmessageオブジェクトから取得した送信元macアドレスをハッシュキーにして物理ポート番号を@fdbオブジェクトに追加します。
learnメソッドについては後ほど見ます。

17行目
@counter.addはCounterクラスのメソッドです。
このメソッドはmessageオブジェクトから取得した送信元のmacアドレスをハッシュキーにして、パケットの送信回数と送信バイト数を@counterオブジェクトに追加します。
addメソッドについては後ほど見ます。

18行目
@fdb.lookupはFDBクラスのメソッドで、送信先のmacアドレスに対応した物理ポート番号を@fdbオブジェクトから取得するメソッドです。
lookupメソッドについては後ほど見ます。

19~24行目
lookupメソッドで物理ポート番号が取得できた場合は以下の処理を行います。
@fdbオブジェクトから物理ポート番号が取得できた場合、以下の処理を行います。
 49行目にあるpacket_outメソッドを実行し、送信先の物理ポート宛てにパケットを送ります。
 40行目にあるflow_modメソッドを実行し、フローテーブルにフローエントリを追加します。

lookupメソッドで物理ポート番号が取得できなかった場合、以下の処理を行います。
 57行目にあるfloodメソッドを実行し、送信元の物理ポート以外の全てのポートにパケットを流します。

27~29行目
flow_removedイベントハンドラです。フローテーブルからフローエントリが削除される時に実行されます。
messageはFlowRemovedクラスのオブジェクトで、ここから削除するフローエントリの情報が取得できます。
@counter.addメソッドでフローテーブルに保持されてていた統計情報を@counterオブジェクトに加算します。

31行目
priveateからは下に書かれたメソッドは外部から参照することはできません

33~38行目
5行目に記述のあったタイマーで実行されるメソッドです
まず、現在時刻を画面に出力します。
35行目から37行目はCounterクラスのeach_pairメソッドを実行しています。
ここではブロック引数という書き方をしていて、doからendまでの処理を@counterクラスのeach_pairメソッドに渡して実行します。
each_pairの処理については後ほど見ます。

40~47行目
flow_modメソッドでは、send_flow_mod_addというフローエントリの追加・変更をするメソッドを実行します。
:hard_timeoutはフローエントリの有効期限を定義しています。ここでは10をセットしているので10秒間フローテーブルに保持されます。
「条件」にあたる「:match」にはMatchオブジェクトを作成して、送信元と送信元のmacアドレスを条件に指定しています。
「処理」にあたる「:actions」には、指定した物理ポート番号に対しパケットを送るようにします。

49~55行目
packet_outメソッドではsend_packet_outメソッドを実行してパケットを送ります。
「:pacekt_in」で送るパケットやパケットの送信元物理ポート等をセットしています。
「:actions」では指定した物理ポート番号にパケットを送るようにします。

floodメソッドは49行目にかかれたpacket_outメソッドを実行し、送信元以外の全ての物理ポートにパケットを送ります。

今回はここまで。
次回につづく。。。

2013年10月9日水曜日

Tremaを試す ~learning switch その2~

今回はlearning-switch.rbから呼ばれているfdb.rbファイルを見ていきます。

その前にRubyのメソッドについて少し説明をしておきます。

Rubyのメソッドは必ず実行結果を返します。
メソッドの途中で返す値をセットしたい場合は「return」を使います。
「return」の後ろに返す式を書けば、そこでメソッドを終了して呼び出し元にその式の値を返すことができます。
「return」を書かなかった場合はメソッドの最後に実行した式の結果を返します。

簡単な例を書きました。

(test.rb)
  def hello
    return "HELLO"
    "good morning"
  end

  def goodby
    "GOOD BY"
  end

  print(hello,"\n")
  print(goodby,"\n")

実行します。

$ ruby test.rb
HELLO
GOOD BY

helloメソッドのようにreturnを書くと、returnの後ろに書いた式の値を返します。
returnの下に書いた"good morning"は実行されません。
goodbyメソッドのようにreturnを書かないとメソッドの最後に実行した"GOOD BY"を返します。

ではfdbのソースを見てみましょう。
examplesの中のlearning_switch配下にあるfdb.rbファイルを見てください。

(fdb.rb)
 1  class ForwardingEntry
 2    include DefaultLogger
 3
 4    attr_reader :mac
 5    attr_reader :port_no
 6    attr_reader :dpid
 7    attr_writer :age_max
 8
 9    def initialize mac, port_no, age_max, dpid
10      @mac = mac
11      @port_no = port_no
12      @age_max = age_max
13      @dpid = dpid
14      @last_update = Time.now
15      debug "New entry: MAC address = #{ @mac.to_s }, port number = #{ @port_no }"
16    end
17
18    def update port_no
19      debug "Update: The port number of #{ @mac.to_s } has been changed #{ @port_no } => #{ port_no }"
20      @port_no = port_no
21      @last_update = Time.now
22    end
23
24    def aged_out?
25      aged_out = Time.now - @last_update > @age_max
26      debug "Age out: An entry (MAC address = #{ @mac.to_s }, port number = #{ @port_no }) has been aged-out" if aged_out
27      aged_out
28    end
29  end
30
31  class FDB
32    DEFAULT_AGE_MAX = 300
33
34    def initialize
35      @db = {}
36    end
37
38    def port_no_of mac
39      dest = @db[ mac ]
40      if dest
41        dest.port_no
42      else
43        nil
44      end
45    end
46
47    def lookup mac
48      if dest = @db[ mac ]
49        [ dest.dpid, dest.port_no ]
50      else
51        nil
52      end
53    end
54
55    def learn mac, port_no, dpid = nil
56      entry = @db[ mac ]
57      if entry
58        entry.update port_no
59      else
60        new_entry = ForwardingEntry.new( mac, port_no, DEFAULT_AGE_MAX, dpid )
61        @db[ new_entry.mac ] = new_entry
62      end
63    end
64
65    def age
66      @db.delete_if do | mac, entry |
67        entry.aged_out?
68      end
69    end
70  end

このソースプログラムにはFDBクラスとForwardingEntryクラスが書かれています。
ForwardingEntryクラスはFDBクラスから呼び出されているので、まずFDBクラスから見ていきましょう。

32行目
DEFAULT_AGE_MAXは変数名を大文字で書いているため定数となります。
300の意味は後ほど説明します。

34~36行目
initializeはクラスのオブジェクトが作成される時に実行されるメソッドです。
処理の内容は@dbというインスタンス変数に空のハッシュ(連想配列)を定義しています。

この@dbに何が入るのかを先に確認しておいた方が、この後の流れがわかりやすいと思います。
ソースをよく見ると60~61行目の処理で、macアドレスをハッシュキーにしてForwardingEntryクラスのオブジェクトを代入している事がわかります。

38~45行目
port_no_ofメソッドは、引数のmacアドレスに紐づく物理ポートの番号を返すメソッドです。
まずは引数のmacアドレスをハッシュキーにして@dbからハッシュ値を取得し、dest変数にセットしています。

if文が書かれていますが条件が何も書かれていません。
これはdest変数がnilかfalseでなければTrueの処理を実行します。
Trueの場合、port_no_ofメソッドはdest変数から物理ポート番号を取得して返します。
Falseの場合、つまりmacアドレスからハッシュ値が取得できなかった場合、port_no_ofメソッドはnilを返します。

47~53行目
lookupメソッドは引数のmacアドレスに紐づくスイッチIDと物理ポート番号を返すメソッドです。

このif文は、引数のmacアドレスをハッシュキーにして@dbからハッシュ値を取得してdest変数に代入すると同時に、dest変数がtrueもしくは値があるか判定しています。
dest変数にハッシュ値が代入されていれば、lookupメソッドはスイッチIDと物理ポート番号を返します。
dest変数にハッシュ値が代入されていなければ、lookupメソッドはnilを返します。

55~63行目
learnメソッドは@dbに要素(ハッシュキー、ハッシュ値)の追加、もしくはハッシュ値を更新するメソッドです。
まず、引数のmacアドレスをハッシュキーにして@dbからハッシュ値を取得し、entry変数にセットしています。
ハッシュ値が取得できた場合は、ForwardingEntryクラスのupdateメソッドを実行してハッシュ値を更新します。
ハッシュ値が取得できなかった場合は、引数のmacアドレスと物理ポート番号、DEFAULT_AGE_MAX定数、スイッチIDを引数にして新たにForwardingEntryクラスのオブジェクトを作り、引数のmacアドレスをハッシュキーにして@dbハッシュに要素を追加しています。

65~69行目
ageメソッドは@dbから一定時間が経過した要素を削除します。
ここでの一定時間は32行目に書かれた定数「DEFAULT_AGE_MAX」の値で、300秒になります。
ハッシュ値の削除はdelete_ifメソッドで行っています。delete_ifは指定した条件にマッチした場合、ハッシュから要素を削除するメソッドです。
条件はForwardingEntryクラスのage_out?メソッドの結果です。
macにはハッシュキー、entryにハッシュ値が入り、@dbの全ての要素に対して条件にマッチするか評価をします。


続いてForwardingEntryクラスを見ていきます。

2行目
includeでモジュールを読み込みます。DefaultLoggerモジュールが使えるようになります。

4~7行目
attr_reader、attre_writerはアクセサと呼ばれるもので、クラスの外からインスタンス変数にアクセスすることができます。
アクセサは「attr_reader :mac」のようにインスタンス変数名から@を外して記述します。
attr_reader(読み出し)、attre_writer(書き込み)の他にattre_accesoer(読み書き)というのがあります。

9~16行目
initializeはForwardingEntryオブジェクトが作成される時に指定した引数をインスタンス変数に代入しています。
@last_update変数にはRuby組み込みメソッドのTimeメソッドを使い現在時刻を代入しています。
debugでmacアドレスと物理ポート番号をログに出力します。

18~22行目
updateメソッドはForwardingEntryオブジェクトの値を更新します。
debugでmacアドレスと物理ポート番号をログに出力します。
@port_no変数の値を引数の物理ポート番号に更新し、@last_update変数の値を現在時刻に書き換えます。

24行目
aged_out?メソッドは、ForwardingEntryオブジェクトに要素を追加もしくはupdateメソッドを実行してから所定の時間が経過しているか否かを返します。
メソッド名の最後に?がついているのはboolean(TrueまたはFalse)を返すメソッドであることを意味しています。
現在時刻から@last_update変数を引いた値が、FDBクラスの「DEFAULT_AGE_MAX」定数の300より大きい場合はaged_out変数にはtrueがセットされます。
26行目の末尾に書かれたifの式の結果がTrueの場合、左側の式が実行されます。 ここではTrueの場合は、debugでmacアドレスと物理ポート番号をログに出力します。
25行目の式の結果(trueかfalse)がaged_out?メソッドが返す値になります。


次に、learning_switchの仮想環境の構成です。

 1  vswitch("lsw") {
 2    datapath_id "0xabc"
 3  }
 4  
 5  vhost ("host1") {
 6    ip "192.168.0.1"
 7    netmask "255.255.0.0"
 8    mac "00:00:00:01:00:01"
 9  }
10  
11  vhost ("host2") {
12    ip "192.168.0.2"
13    netmask "255.255.0.0"
14    mac "00:00:00:01:00:02"
15  }
16  
17  link "lsw", "host1"
18  link "lsw", "host2"

1~15行目
スイッチ1台、ホスト2台の構成になっています。

17~18行目
スイッチとホスト1、スイッチとホスト2を接続する構成にしています。


learning_switch.rbは、スイッチが未知のパケットを受け取るとコントローラは@fdbオブジェクトに送信元のmacアドレスと物理ポート番号を記録します。
また、スイッチが未知のパケットを受け取った時、@fdbオブジェクトに送信先のmacアドレスと物理ポート番号が記録されていればflow_modメソッドを実行しフローエントリを追加します。
これらの動きを確認したいと思います。


では、以下のコマンドでlearning_switchを実行してみましょう。

trema run ./learning-switch.rb -c ./learning_switch.conf

learning_switchを実行した端末の他にもう一つ端末を起動して仮想環境の確認をします。
以下のコマンドでフローテーブルの情報を表示します。

trema dump_flows lsw

まだフローテーブルにエントリは無いので以下のような表示になります。

$ trema dump_flows lsw
NXST_FLOW reply (xid=0x4):

次に、ホスト1からホスト2に向けてパケットを送り、ホスト1とホスト2のパケット送受信状況を確認します。
パケットを送るには以下のコマンドを実行します。

trema send_packets --source [送信元] --dest [送信先]

送受信状況の確認には以下のコマンドを実行します。

trema show_stats [ホスト名]

以下のように表示されます。

$ trema send_packets --source host1 --dest host2
$ trema show_stats host1
Sent packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,1,50
Received packets:

$ trema show_stats host2
Sent packets:

Received packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,1,50


これで@fdbにホスト1のmacアドレスと物理ポート番号が記録されたはずです。
再びフローテーブルを表示します。

$ trema dump_flows lsw
NXST_FLOW reply (xid=0x4):

まだフローエントリはありません。
今度はホスト2からホスト1に向けてパケットを送り、ホスト1とホスト2のパケット送受信状況を確認します。

$ trema send_packets --source host2 --dest host1
$ trema show_stats host1
Sent packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,1,50
Received packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50
$ trema show_stats host2
Sent packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50
Received packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,1,50


フローテーブルを表示します。

$ trema dump_flows lsw
NXST_FLOW reply (xid=0x4):
cookie=0x1, duration=30.414s, table=0, n_packets=0, n_bytes=0, priority=65535,udp,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:01:00:02,dl_dst=00:00:00:01:00:01,nw_src=192.168.0.2,nw_dst=192.168.0.1,nw_tos=0,tp_src=1,tp_dst=1 actions=output:2

今度はフローエントリが追加されています。

ホスト2からホスト1へパケットを送ると、ホスト2のmacアドレスと物理ポート番号が@fdbに記録されるとともに、ホスト1のmacアドレスと物理ポート番号は既に@fdbに記録されていたためホスト2からホスト1へのフローエントリが追加されます。

今度はホスト1からホスト2へのフローエントリが追加されるか確認します。
ホスト1からホスト2へパケットを送ります。

$ trema send_packets --source host1 --dest host2
$ trema show_stats host1
Sent packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,2,100
Received packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50
$ trema show_stats host2
Sent packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50
Received packets:
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.2,1,192.168.0.1,1,2,100


フローテーブルを表示すると、今度はホスト1からホスト2へのフローエントリーが追加されました。

$ trema dump_flows lsw
NXST_FLOW reply (xid=0x4):
 cookie=0x1, duration=77.173s, table=0, n_packets=0, n_bytes=0, priority=65535,udp,in_port=1,vlan_tci=0x0000,dl_src=00:00:00:01:00:02,dl_dst=00:00:00:01:00:01,nw_src=192.168.0.2,nw_dst=192.168.0.1,nw_tos=0,tp_src=1,tp_dst=1 actions=output:2
 cookie=0x2, duration=25.139s, table=0, n_packets=0, n_bytes=0, priority=65535,udp,in_port=2,vlan_tci=0x0000,dl_src=00:00:00:01:00:01,dl_dst=00:00:00:01:00:02,nw_src=192.168.0.1,nw_dst=192.168.0.2,nw_tos=0,tp_src=1,tp_dst=1 actions=output:1


今回は以上になります。