OpenDaylightサンプルソースの解説

こんにちは

今回はSDNシリーズの四回目になります。

一回目は、SDNとは何なのかを簡単な応用例を示して説明しました。
二回目は、OpenDaylightとのインストールと、MininetのセットアップおよびOpenDaylightへの接続と疎通確認まで実施しました。
また、前回はOpenDaylightのサンプルアプリのビルドと、そのアプリを用いてMininet上に作成したスイッチの制御を実行してみました。

今回は前回ビルドしたサンプルアプリの解説をしたいと思います。
(長くなったので、Mininetコマンドの解説は次回にさせていただきます。申し訳ありません。)

OpenDaylightのアーキテクチャ

まずはOpenDaylightアーキテクチャの解説です。

以下の図は二回目で紹介した図ですが、まずこのうち”Base Network Service Functions”、SAL(Service Abstraction Layer)、およびOpenFlowについて解説します。

図1
図1(出典:Technical Overview | OpenDaylight

Base Network Service Functions

ネットワーク上のデバイス情報を収集するためのサービス群です。
以下、各バンドルの解説です。

バンドルAPI説明
ARP HandlerIHostFinderARPを処理してホストの場所を学習する
Host TrackerIflptoHostSDN上でホストの相対的な場所を追跡する
Switch ManagerISwitchManagerコントローラ内のすべてのスイッチのリストを保持する
Topology ManagerITopologyManagerネットワーク全体のトポロジを保持する
Stats ManagerIStatisticsManagerIReadServiceを利用して統計情報を収集する
FRMIForwardingRulesManagerフローデータベースにアクセスする※
User ManagerIUserManagerユーザ管理を担う

ソース上のコメントより。

SAL(Service Abstraction Layer)

SDN対応ネットワーク機器にアクセスするためのAPIです。
以下、各クラスの解説です。

Class説明
ActionOpenFlowのフローエントリに設定するアクション
MatchOpenFlowのフローエントリに設定するマッチングルール
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の動作です。

  1. スイッチからのPacket-in受信を契機として処理開始
  2. 受信パケットから以下の情報を取得
    1. 送信元MACアドレス
    2. 送信元ポート番号
    3. 宛先MACアドレス
  3. 取得した送信元MACアドレスと送信元ポート番号の対をHashMapに格納
  4. 宛先MACアドレスを元にHashMapから宛先ポート番号を取得
    1. 取得できなかった場合
      受信元ポート番号以外のすべてのポートにパケットを送信(floodPacket)
    2. 取得できた場合
      1. 送信元ポート番号と宛先MACアドレスをそれぞれMatchリストに追加
      2. 取得できた送信先ポート番号からパケットを送出するアクションをActionに登録
      3. 上記のMatchリストとActionをフローエントリに追加
      4. 取得したポート番号のポートに対してパケットを送出

このサンプルアプリは、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コマンドの解説は次回にします。
以上です。