こんにちは
今回はSDNシリーズの四回目になります。
一回目は、SDNとは何なのかを簡単な応用例を示して説明しました。
二回目は、OpenDaylightとのインストールと、MininetのセットアップおよびOpenDaylightへの接続と疎通確認まで実施しました。
また、前回はOpenDaylightのサンプルアプリのビルドと、そのアプリを用いてMininet上に作成したスイッチの制御を実行してみました。
今回は前回ビルドしたサンプルアプリの解説をしたいと思います。
(長くなったので、Mininetコマンドの解説は次回にさせていただきます。申し訳ありません。)
OpenDaylightのアーキテクチャ
まずはOpenDaylightアーキテクチャの解説です。
以下の図は二回目で紹介した図ですが、まずこのうち”Base Network Service Functions”、SAL(Service Abstraction Layer)、およびOpenFlowについて解説します。
Base Network Service Functions
ネットワーク上のデバイス情報を収集するためのサービス群です。
以下、各バンドルの解説です。
バンドル | API | 説明 |
ARP Handler | IHostFinder | ARPを処理してホストの場所を学習する |
Host Tracker | IflptoHost | SDN上でホストの相対的な場所を追跡する |
Switch Manager | ISwitchManager | コントローラ内のすべてのスイッチのリストを保持する |
Topology Manager | ITopologyManager | ネットワーク全体のトポロジを保持する |
Stats Manager | IStatisticsManager | IReadServiceを利用して統計情報を収集する |
FRM | IForwardingRulesManager | フローデータベースにアクセスする※ |
User Manager | IUserManager | ユーザ管理を担う |
※ソース上のコメントより。
SAL(Service Abstraction Layer)
SDN対応ネットワーク機器にアクセスするためのAPIです。
以下、各クラスの解説です。
Class | 説明 |
Action | OpenFlowのフローエントリに設定するアクション |
Match | OpenFlowのフローエントリに設定するマッチングルール |
IFlowProgrammerService | スイッチに対してフローエントリの追加更新削除を行う |
IDataPacketService | パケット操作のためのサービス OpenFlowのForwardアクションを発行する |
IReadService | スイッチのフロー/ポート/キューのビューを取得する |
ITopologyService | ネットワーク上で新しいノードやリンクなどを検出した場合に、その情報を伝える |
IDataPacketService | パケットデコード等、パケット操作を行う |
IListenDataPacket | パケット処理のためのクラス このクラスを継承してコントローラクラスを作成する |
OpenFlow
前回はMininet上に仮想的なスイッチとホストを作成して、OpenDaylight使ってそのスイッチを制御する方法を示しました。
二回目の記事で紹介したように、MininetはOpenFlow対応の仮想ネットワークデバイスを生成するツールです。
従って、前回ビルドしたControllerアプリもOpenFlowアプリになりますので、ここでOpenFlowの解説をしておきます。
OpenFlowとOpenDaylightの関係は、図1の左下に記載されているように、OpenDaylightでサポートしているプロトコルの1つになります。
OpenFlowの詳細については、多くの日本語サイトがありますので、そちらでご確認願います。
ここでは、サンプルアプリの説明のため、OpenFlowについては以下の点のみ抑えておきます。
- パケットの制御は、ヘッダフィールドのマッチングルールとアクションで実施する。
※ここのマッチングルールとアクションをハンドリングするOpenDaylightのクラスが、表2のMatchとActionです。 - コントローラとネットワーク機器の間では、パケットの制御情報をメッセージ(OpenFlowメッセージ)によってやり取りする。
サンプルアプリ(tutorial_L2_forwarding)
上記のOpenDaylightのサービス群やクラスおよびOpenFlowに対する説明を踏まえて、サンプルアプリの解説をしていきたいと思います。
簡単な流れは以下のようになっています。※これは、Learning Switchの動作です。
- スイッチからのPacket-in受信を契機として処理開始
- 受信パケットから以下の情報を取得
- 送信元MACアドレス
- 送信元ポート番号
- 宛先MACアドレス
- 取得した送信元MACアドレスと送信元ポート番号の対をHashMapに格納
- 宛先MACアドレスを元にHashMapから宛先ポート番号を取得
- 取得できなかった場合
受信元ポート番号以外のすべてのポートにパケットを送信(floodPacket) - 取得できた場合
- 送信元ポート番号と宛先MACアドレスをそれぞれMatchリストに追加
- 取得できた送信先ポート番号からパケットを送出するアクションをActionに登録
- 上記のMatchリストとActionをフローエントリに追加
- 取得したポート番号のポートに対してパケットを送出
- 取得できなかった場合
このサンプルアプリは、TutorialL2Forwarding.java、およびActivator.javaの二つのモジュールから成り立っています。
このうち、Activator.javaはOSGiフレームワークを使用したバンドルアクティベータで、本体はTutorialL2Forwarding.javaに記述されています。
以下、ソースの解説です。
TutorialL2Forwarding.java
はじめに前提として以下の行を書き換えます。※これをしないと、サンプルコントローラは単なるRepeater Hubになります。
[java firstline="74"]private String function = "hub";[/java]
↓
[java firstline="74"]private String function = "switch";[/java]
このクラスはSouthbound APIからパケットを取得するために必要なOpenDaylightのIListenDataPacketクラスを継承します。
(List1)
[java firstline="67"] public class TutorialL2Forwarding implements IListenDataPacket { [/java]
1.Packet-in
IListenDataPacketクラスのreceiveDataPacketメソッドをオーバーライドすることにより、パケットのコピーを取得します(List2 182行目)。
このメソッドはOpenFlowのPacket-inイベントが発生すると呼び出されます。
2.送信元接続ポート抽出、受信パケット復号
receiveDataPacketメソッドの引数として渡されたRawPacketのgetIncomingNodeConnectorメソッドを用いて送信元接続ポートを抽出します。(List2 187行目)
次に、dataPacketServiceを使用してパケットを復号し、パケットの各フィールドへアクセスするために、org.opendaylight.controller.sal.packet.IDataPacketServiceクラスのdecodeDataPacketメソッドを使用して、Packetオブジェクトに変換します。(List2 193行目)
(List2)
[java firstline="181" highlight="182,187,193,198,200,202,204,205,209,212"] @Override public PacketResult receiveDataPacket(RawPacket inPkt) { if (inPkt == null) { return PacketResult.IGNORED; } NodeConnector incoming_connector = inPkt.getIncomingNodeConnector(); // Hub implementation if (function.equals("hub")) { floodPacket(inPkt); } else { Packet formattedPak = this.dataPacketService.decodeDataPacket(inPkt); if (!(formattedPak instanceof Ethernet)) { return PacketResult.IGNORED; } learnSourceMAC(formattedPak, incoming_connector); NodeConnector outgoing_connector = knowDestinationMAC(formattedPak); if (outgoing_connector == null) { floodPacket(inPkt); } else { if (!programFlow(formattedPak, incoming_connector, outgoing_connector)) { return PacketResult.IGNORED; } inPkt.setOutgoingNodeConnector(outgoing_connector); this.dataPacketService.transmitDataPacket(inPkt); } } return PacketResult.CONSUME; } [/java]
3.送信元情報の取得
learnSourceMACローカルメソッドでMACアドレスと入力ポートの対をHashMapに格納する。(List2 198行目、List3 215-218行目)
復号したパケットからorg.opendaylight.controller.sal.packet.EthernetクラスのgetSourceMACAddressメソッドとorg.opendaylight.controller.sal.packet.BitBufferHelperのtoNumberメソッドを用いて送信元MACアドレスを取り出す。(216、217行目)
送信元MACアドレスと送信元接続ポートの対をHashMapに格納。(218行目)
(List3)
[java firstline="215"] private void learnSourceMAC(Packet formattedPak, NodeConnector incoming_connector) { byte[] srcMAC = ((Ethernet)formattedPak).getSourceMACAddress(); long srcMAC_val = BitBufferHelper.toNumber(srcMAC); this.mac_to_port.put(srcMAC_val, incoming_connector); } [/java]
4.宛先MACアドレス取得、接続ポート判定
knowDestinationMACローカルメソッドで内部のHashMapから宛先MACアドレスを取り出す。(List2 200行目)org.opendaylight.controller.sal.packet.EthernetクラスのgetDestinationMACAddressメソッドで宛先MACアドレスを取り出す。(222、223行目)
取り出した宛先MACアドレスの接続ポートを返却。(224行目)
(List4)
[java firstline="221"] private NodeConnector knowDestinationMAC(Packet formattedPak) { byte[] dstMAC = ((Ethernet)formattedPak).getDestinationMACAddress(); long dstMAC_val = BitBufferHelper.toNumber(dstMAC); return this.mac_to_port.get(dstMAC_val) ; } [/java]
5.foodPacket
List4 knowDestinationMACローカルメソッドの戻り値で宛先MACアドレスの接続ポートが取得できなかった場合、
(宛先MACアドレスの接続ポートを学習していなかった場合)はfloodPacketローカルメソッドで送信元以外のすべてのポートへパケットを送出する。(List2 202行目)
floodPacketローカルメソッドでは、ISwitchManagerクラスのgetUpNodeConnectorsメソッドを使用してスイッチ内のすべてのポートを抽出(List5 163、164行目)し、抽出したポートの数分(送信元ポートを除く)IDataPacketServiceクラスのtransmitDataPacketメソッドを使用してパケットを送出する。(List5 171行目)
(List5)
[java firstline="159" highlight="163,164,171"] private void floodPacket(RawPacket inPkt) { NodeConnector incoming_connector = inPkt.getIncomingNodeConnector(); Node incoming_node = incoming_connector.getNode(); Set<NodeConnector> nodeConnectors = this.switchManager.getUpNodeConnectors(incoming_node); for (NodeConnector p : nodeConnectors) { if (!p.equals(incoming_connector)) { try { RawPacket destPkt = new RawPacket(inPkt); destPkt.setOutgoingNodeConnector(p); this.dataPacketService.transmitDataPacket(destPkt); } catch (ConstructionException e2) { continue; } } } } [/java]
6.フローエントリ組み立て、フローテーブル書込
宛先MACアドレスの接続ポートが取得できた場合、programFlowローカルメソッドを呼び出し、フローエントリの組み立てとスイッチへの登録を行う。(List2 204、205行目)
programFlowローカルメソッドでは、Matchクラスを使用して、マッチングルールを作成。(List6 232-234行目)
ここでは、入力ポートが今回の送信元ポートであること(List6 233行目)と、宛先MACアドレスが今回の宛先MACアドレスであること(List6 234行目)となっています。
(List6)
[java firstline="227" highlight="232-234,237,239,244"] private boolean programFlow(Packet formattedPak, NodeConnector incoming_connector, NodeConnector outgoing_connector) { byte[] dstMAC = ((Ethernet)formattedPak).getDestinationMACAddress(); Match match = new Match(); match.setField( new MatchField(MatchType.IN_PORT, incoming_connector) ); match.setField( new MatchField(MatchType.DL_DST, dstMAC.clone()) ); List<Action> actions = new ArrayList<Action>(); actions.add(new Output(outgoing_connector)); Flow f = new Flow(match, actions); f.setIdleTimeout((short)5); // Modify the flow on the network node Node incoming_node = incoming_connector.getNode(); Status status = programmer.addFlow(incoming_node, f); if (!status.isSuccess()) { logger.warn("SDN Plugin failed to program the flow: {}. The failure is: {}", f, status.getDescription()); return false; } else { return true; } } [/java]
次に、アクションの作成をします。
ここでは今回の宛先ポートからパケットを送出するルールになっています。(List6 237行目)
これらマッチングルールとアクションをFlowクラスに格納(List6 239行目)し、IFlowProgrammerServiceクラスのaddFlowメソッドに渡して実際にスイッチにフローエントリを挿入しています。(List6 244行目)
7.パケット送出
フローエントリの挿入に成功したら、入力パケットを出力ポートから送出します。(List2 209行目)
8.処理終了
最後にパケットが正常に処理されたことを示すPacketResult.CONSUMEを返して処理を終了します。(List2 212行目)
終わりに
今回はサンプルソースの解説だけで大きくなってしまったので、予定していたMininetコマンドの解説は次回にします。
以上です。