こんにちは
今回は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コマンドの解説は次回にします。
以上です。

