今日も秋田で IoT

旧 Trema 日記

Features Request でスイッチから情報を取得する (その3)

今回は Trema の話はすこしお休みして、OpenFlow spec 1.0 の仕様書を見てみましょう。

Features Reply の中身は以下のように定義されています。

/* Switch features. */
struct ofp_switch_features {
    struct ofp_header header;
    uint64_t datapath_id;   /* Datapath unique ID.  The lower 48-bits are for
                               a MAC address, while the upper 16-bits are
                               implementer-defined. */

    uint32_t n_buffers;     /* Max packets buffered at once. */

    uint8_t n_tables;       /* Number of tables supported by datapath. */
    uint8_t pad[3];         /* Align to 64-bits. */

    /* Features. */
    uint32_t capabilities;  /* Bitmap of support "ofp_capabilities". */
    uint32_t actions;       /* Bitmap of supported "ofp_action_type"s. */

    /* Port info.*/
    struct ofp_phy_port ports[0];  /* Port definitions.  The number of ports
                                      is inferred from the length field in
                                      the header. */
};
OFP_ASSERT(sizeof(struct ofp_switch_features) == 32);
  • datapath_id : 言わずと知れた Datapath ID です。
  • n_buffers : バッファのサイズが格納されます。
  • n_tables : フローテーブルの数が返されます。OpenFlow 1.1 以降であれば複数のテーブルを扱うことができますが、OpenFlow 1.0 では一つしか扱えません。ですので、ここには 1 が格納されるはずです。
  • capabilities : スイッチのケイパビリティを示すビットマップが返されます(後述)。
  • actions : スイッチがサポートするアクション一覧を示すビットマップが返されます(後述)。
  • ports : スイッチが持つポート一覧が返されます。

スイッチのケイパビリティ

capabilities で返されるビットマップの各ビットは、以下のように定義されています。

enum ofp_capabilities {
    OFPC_FLOW_STATS     = 1 << 0,  /* Flow statistics. */
    OFPC_TABLE_STATS    = 1 << 1,  /* Table statistics. */
    OFPC_PORT_STATS     = 1 << 2,  /* Port statistics. */
    OFPC_STP            = 1 << 3,  /* 802.1d spanning tree. */
    OFPC_RESERVED       = 1 << 4,  /* Reserved, must be zero. */
    OFPC_IP_REASM       = 1 << 5,  /* Can reassemble IP fragments. */
    OFPC_QUEUE_STATS    = 1 << 6,  /* Queue statistics. */
    OFPC_ARP_MATCH_IP   = 1 << 7   /* Match IP addresses in ARP pkts. */
};
  • OFPC_FLOW_STATS, OFPC_TABLE_STATS, OFPC_PORT_STATS, OFPC_QUEUE_STATS : Stats Request/Reply による統計情報の取得に対応しているか否かを示すビットです。
  • OFPC_STP : スパニングツリープロトコルへの対応を示します。
  • OFPC_IP_REASM : フラグメントされた IP パケットのリアセンブルに対応しているか否かを示します。
  • OFPC_ARP_MATCH_IP : ARP 中の IP アドレスへのマッチに対応しているかを示します。

最後の三つについては、どのような機能なのか説明が難しいですが、知らなくても特に困らないでしょう。時間があればそのうち試して、謎を解明してみたいと思います。

スイッチがサポートするアクション

actions で返されるビットマップの各ビットの意味は、ofp_action_type の定義に対応します。

enum ofp_action_type {
    OFPAT_OUTPUT,           /* Output to switch port. */
    OFPAT_SET_VLAN_VID,     /* Set the 802.1q VLAN id. */
    OFPAT_SET_VLAN_PCP,     /* Set the 802.1q priority. */
    OFPAT_STRIP_VLAN,       /* Strip the 802.1q header. */
    OFPAT_SET_DL_SRC,       /* Ethernet source address. */
    OFPAT_SET_DL_DST,       /* Ethernet destination address. */
    OFPAT_SET_NW_SRC,       /* IP source address. */
    OFPAT_SET_NW_DST,       /* IP destination address. */
    OFPAT_SET_NW_TOS,       /* IP ToS (DSCP field, 6 bits). */
    OFPAT_SET_TP_SRC,       /* TCP/UDP source port. */
    OFPAT_SET_TP_DST,       /* TCP/UDP destination port. */
    OFPAT_ENQUEUE,          /* Output to queue.  */
    OFPAT_VENDOR = 0xffff
};

ビットマップの定義になっていませんが、OUTPUT が 1<<0, SET_VLAN_VID が 1<<1 という具合に対応していると考えればよいでしょう。

おわりに

今回説明できなかった Features Request/Reply で取得できるポートの情報に関しては、次回詳しく見ていきます。

Features Request でスイッチから情報を取得する (その2)

前回、Ruby でコントローラを書きましたが、もちろん C 言語でも同様なことができます。

#include <stdio.h>
#include "trema.h"


static void
handle_switch_ready( uint64_t dpid, void *user_data )
{
  UNUSED( user_data );

  buffer *features_request = create_features_request( get_transaction_id() );

  send_openflow_message( dpid, features_request );
  free_buffer( features_request );
}


static void
handle_features_reply( uint64_t dpid, uint32_t txid, uint32_t n_buffers, uint8_t n_tables, uint32_t capabilities, uint32_t actions, const list_element *phy_ports, void *user_data )
{
  UNUSED( user_data );
  UNUSED( n_buffers );
  UNUSED( n_tables );
  UNUSED( actions );
  UNUSED( txid );

  printf( "Datapath ID: %#" PRIx64 "\n", dpid );

  const list_element *p;  
  for ( p = phy_ports; p != NULL; p = p->next ) {
    struct ofp_phy_port *port = p->data;
    printf( "Port no: %u\n", port->port_no );
    printf( "  Hardware address: %02x:%02x:%02x:%02x:%02x:%02x\n", 
            port->hw_addr[0], port->hw_addr[1], port->hw_addr[2],
            port->hw_addr[3], port->hw_addr[4], port->hw_addr[5] );
    printf( "  Port name: %s\n", port->name );
  }
}


int
main( int argc, char *argv[] )
{
  init_trema( &argc, &argv );
  set_switch_ready_handler( handle_switch_ready, NULL );
  set_features_reply_handler( handle_features_reply, NULL );
  start_trema();
 
  return 0;
}

上記のファイルを show_features.c として保存し、以下のようにコンパイル、実行をします。

$ gcc `trema-config --cflag` -c show_features.c
$ gcc show_features.o `trema-config --libs` -o show_features
$ sudo trema run ./show_features -c ./network.conf 
Datapath ID: 0xabc
Port no: 2
  Hardware address: f6:1d:1b:91:e6:ae
  Port name: trema0-1
Port no: 65534
  Hardware address: 8e:14:20:6f:54:15
  Port name: vsw_0xabc
Port no: 1
  Hardware address: b2:69:85:cf:91:aa
  Port name: trema1-1
^C
terminated

Ruby と比べると、コードが長くなりますが、同じ実行結果を得ることができるはずです。

次回、OpenFlow の仕様を参考に、Features Request でどのような情報が表示できるかを見ていきましょう。

Features Request でスイッチから情報を取得する (その1)

OpenFlow プロトコルでは、スイッチから情報を取得するために、Features Request/Reply メッセージが定義されています。

今回は、これらのメッセージを用いたコントローラを作り、実行することで、どのような情報が取得できるかを確認してみます。

class ShowFeatures < Controller
  def switch_ready datapath_id
    send_message datapath_id, FeaturesRequest.new
  end

  def features_reply datapath_id, message
    puts "Datapath ID: #{ datapath_id.to_hex }"

    message.ports.each do | each |
      puts "Port no: #{ each.number }"
      puts "  Hardware address: #{ each.hw_addr.to_s }"
      puts "  Port name: #{ each.name }"
    end
  end
end

上記を show-features.rb として保存します。

vswitch { dpid "0xabc" }
vhost ( "host1" )
vhost ( "host2" )
link "host1", "0xabc"
link "host2", "0xabc"

ネットワークエミュレータ用に上記のファイル (network.conf) を用意して、以下のように trema で実行してみましょう。

% sudo trema run ./show-features.rb -c ./network.conf
Datapath ID: 0xabc
Port no: 2
  Hardware address: fe:42:ea:3d:3a:8a
  Port name: trema0-1
Port no: 65534
  Hardware address: ce:47:69:a5:7e:e0
  Port name: vsw_0xabc
Port no: 1
  Hardware address: 46:e0:4e:93:24:79
  Port name: trema1-1
^C
terminated

上記のように、スイッチ 0xabc のポート情報が表示されるはずです。このスイッチには、network.conf 内で定義した二つの vhost を収容するためのポート(trema1-1, trema0-1) と、ポート番号が 65534 のポート (vsw_0xabc) が準備されていることが分かります。

次回、取得できる情報について詳しい説明をしたいと思います。

Sliceable Switch を使う (その2)

Sliceable Switch のスライス設定は、前節で紹介したようにコマンドで行うこともできますが、REST ベースの API で行うこともできます。今回はこの REST API を使った設定方法を、試しながら紹介していきたいと思います。
f:id:kazuya_ax:20120713183033p:image:w250
まずは、コマンド設定と REST API 経由での設定との関係を理解するために、各モジュール間の関係についてすこし説明します。図に示すように、スライスの設定は、slice.db という名前の sqlite3 のデータベースに格納されています。sliceable_switch モジュールはこのデータベースから、スライス設定を取得しています。先ほど紹介した slice コマンドを実行すると、スライスの設定がこのデータベースに書き込まれます。
Sliceable Switch の REST API は、Apache 上で動作する CGI で実現しています。HTTP クライアントからアクセスすると、config.cgi が呼び出され、パースした結果を slice.db へと書き込みます。

準備

REST API を使用するためには、少し準備が必要です。まずは必要なモジュールのインストールを行いましょう。

% sudo apt-get install sqlite3 libdbi-perl libdbd-sqlite3-perl
% sudo apt-get install apache2-mpm-prefork libjson-perl

次に Apache の設定を行います。必要な設定ファイル等は同梱されていますので、以下の手順を実施してください。

% cd apps/sliceable_switch
% sudo cp apache/sliceable_switch /etc/apache2/sites-available
% sudo a2enmod rewrite actions
% sudo a2ensite sliceable_switch

次に、Sliceable Switch が参照するデータベースと、CGI 経由でデータベースに書き込むためのスクリプト群を用意し、適切なディレクトリに配置します。そして最後に Apache の再起動を行います。

% ./create_tables.sh
A filter entry is added successfully.
% sudo mkdir -p /home/sliceable_switch/script
% sudo mkdir /home/sliceable_switch/db
% sudo cp Slice.pm Filter.pm config.cgi /home/sliceable_switch/script
% sudo cp *.db /home/sliceable_switch/db
% sudo chown -R www-data.www-data /home/sliceable_switch
% sudo /etc/init.d/apache2 reload

これで設定は終了です。最後に 各ファイルが適切に配置されていることを確認してください。

% ls /home/sliceable_switch/*
/home/sliceable_switch/db:
filter.db  slice.db

/home/sliceable_switch/script:
Filter.pm  Slice.pm  config.cgi

REST API を使ってみる

まず、スライスを作ってみましょう。slice1 という ID のスライスを作る場合には、JSON 形式のファイル (slice.json) を用意します。

{
  "id" : "slice1",
  "description" : "Trema-team network"
}

この JSON 形式のファイルを、/networks という URI に POST メソッドで送ることで、スライスを作ることができます。
httpc というテスト用の HTTP クライアントが ./test/rest_if/ のディレクトリ配下に用意されていますので、これを用います。今回の設定では Apache の待ち受けポートは 8888 になっていますので、以下のように実行してみましょう。

% ./test/rest_if/httpc POST http://127.0.0.1:8888/networks ./slice.json
Status: 202 Accepted
Content:
{"id":"slice1","description":"Trema-team network"}

成功すれば、上記のように表示されるはずです。
次に、MAC アドレスをスライスに対応させてみましょう。JSON 形式で、割り当てる MAC アドレスを指定したファイルを用意します(attachment.json)。

{
  "id" : "attach0",
  "mac" : "01:00:00:00:00:01"
}

スライスに MAC アドレスを割り当てる際に使用する URI は、/networks/<スライスID>/attachments です。以下のように attachment.json を slice1 に割り当てます。

% ./test/rest_if/httpc POST http://127.0.0.1:8888/networks/slice1/attachments attachment.json
Status: 202 Accepted

今度は、ポートをスライスに割り当てる方法を見ていきましょう。これまでと同様に、JSON 形式で、割り当てるポートに関する情報を記載したファイルを用意します(port.json)。ここでは、データパス ID が 0xe0 である OpenFlow スイッチの 33 番目のポートを指定しています。このポートからパケットを出す際に VLAN tag を付与したい場合には vid のパラメータにその値を設定します。VLAN tag の設定が不要の場合には、この例のように 65535 としてください。

{
  "id" : "port0",
  "datapath_id" : "0xe0",
  "port" : 33,
  "vid" : 65535
}

このとき使用する URI は /networks/<スライスID>/ports になります。以下のようにして、port.json で指定したポートを slice1 というスライスにを割り当てます。

% ./test/rest_if/httpc POST http://127.0.0.1:8888/networks/slice1/ports ./port.json
Status: 202 Accepted

これまでの設定がきちんと行われているかを確認してみましょう。/networks/<スライスID> に GET メソッドでアクセスすることで、スライスに関する情報を取得できます。slice1 に関する情報を取得してみましょう。

% ./test/rest_if/httpc GET http://127.0.0.1:8888/networks/slice1
Status: 200 OK
Content:
{ "bindings" : 
  [
    {
      "type" : 2,
      "id" : "attach0",
      "mac" : "01:00:00:00:00:01"
    },
    {
      "vid" : 65535,
      "datapath_id" : "224",
      "type" : 1,
      "id" : "port0",
      "port" : 33 
    }
  ],
  "description" : "Trema-team network"
}

この出力結果は見やすいようにインデント表示にしていますが、実際には改行なしで表示されます。先に設定した内容が、きちんと反映されているかの確認ができます。

おわりに

今回は Sliceable Switch の REST API 設定を試しながら、紹介を行いました。もし、試してみて動かないところや、間違っているところを見つけたら、ぜひコメント等でご指摘ください。
次回は、補足的に、今回説明できなかったことの解説を行います。

Sliceable Switch を使う (その1)

今回は Trema Apps にある Sliceable Switch について紹介します。このアプリを使うと、OpenFlow ネットワーク全体をスライスに分割し,複数の L2 ネットワークとして動かすことができます。ちょうど,L2 スイッチを複数の VLAN に分けて使うイメージです。マルチテナントを収容するデータセンターで、テナント毎に独立したネットワークを実現するなどの用途を想定したものです。

準備

Sliceable Switch のソースコードhttps://github.com/trema/apps/ にて公開されています。ソースコードをまだ取得していない場合は、git で取得してください。ここでは、以下のようなディレクトリ構成になっていることが前提です。

% ls -F
apps/   trema/

Sliceable Switch は、topology や flow_manager とも連動して動作するようになっていますので、これらのモジュールも合わせて make してください。

% (cd apps/topology/; make)
% (cd apps/flow_manager/; make)
% (cd apps/sliceable_switch; make)

動作させる

まずは以下の内容のファイルを、network.conf という名前で用意してください。

vswitch {
  datapath_id "0xe0"
}

vhost ("host1") {
  ip "192.168.0.1"
  netmask "255.255.0.0"
  mac "00:00:00:00:00:01"
}

vhost ("host2") {
  ip "192.168.0.2"
  netmask "255.255.0.0"
  mac "00:00:00:00:00:02"
}

vhost ("host3") {
  ip "192.168.0.3"
  netmask "255.255.0.0"
  mac "00:00:00:00:00:03"
}

vhost ("host4") {
  ip "192.168.0.4"
  netmask "255.255.0.0"
  mac "00:00:00:00:00:04"
}

link "0xe0", "host1"
link "0xe0", "host2"
link "0xe0", "host3"
link "0xe0", "host4"

run {
  path "../apps/topology/topology"
}

run {
  path "../apps/topology/topology_discovery"
}

run {
  path "../apps/flow_manager/flow_manager"
}

run {
  path "../apps/sliceable_switch/sliceable_switch"
  options "-s", "../apps/sliceable_switch/slice.db", "-f", "../apps/sliceable_switch/filter.db"
}

event :port_status => "topology", :packet_in => "filter", :state_notify => "topology"
filter :lldp => "topology_discovery", :packet_in => "sliceable_switch"

trema のディレクトリに移動し、Sliceable Switch を起動します。
Siceable Switch の起動には、ルート権限が必要です。sudo を使って、以下のように起動してください。

% cd ../../trema
% sudo ./trema run -c ./network.conf

このように起動しただけでは、スライス機能つきスイッチは動作しません。スライスの設定が必要です。今回は二つのスライスを作ってみましょう。
スライスの作成には、同梱されている slice コマンドを使用します。このコマンドを使って、以下のように、二つのスライス slice1, slice2 を作ってみましょう。

% cd ../apps/sliceable_switch
% ./slice create slice1
A new slice is created successfully.
% ./slice create slice2
A new slice is created successfully.

次は作成したそれぞれのスライスにホストを所属させます。その方法には、ホストが接続しているポートの指定と、ホストの MAC アドレスの登録の二通りがあります。今回は後者の方法で試してみましょう。以下のように host1, host2 の MAC アドレスを slice1 に、host3, host4 の MAC アドレスを slice2 に、それぞれ登録を行います。

% ./slice add-mac slice1 00:00:00:00:00:01
A MAC-based binding is added successfully.
% ./slice add-mac slice1 00:00:00:00:00:02
A MAC-based binding is added successfully.
% ./slice add-mac slice2 00:00:00:00:00:03
A MAC-based binding is added successfully.
% ./slice add-mac slice2 00:00:00:00:00:04
A MAC-based binding is added successfully.

ここまでで準備は完了です。それではまず同じスライスに所属するホスト同士が通信できることを確認してみましょう。
MAC アドレスをスライスに登録する方法では、コントローラは起動直後に、登録した MAC アドレスを持つホストがどこにいるのかを知りません。そのため、host1 の位置をコントローラに学習させるために、はじめに host1 から host2 へとパケットを送ります。その後、host2 から host1 へパケットを送り、host1 の受信カウンタを見てみましょう。host2 からのパケットが受信できていることが確認できます。

% cd ../../trema
% ./trema send_packet --source host1 --dest host2
% ./trema send_packet --source host2 --dest host1
% ./trema show_stats host1 --rx
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50

次に、異なるスライスに所属するホスト同士は通信できないことを確認してみます。slice2 に所属する host4 から slice1 に所属するhost1 へとパケットを送ってみましょう。host1 の受信カウンタを見ても、host4 からのパケットが届いていないことがわかります。

% ./trema send_packet --source host4 --dest host1
% ./trema show_stats host1 --rx
ip_dst,tp_dst,ip_src,tp_src,n_pkts,n_octets
192.168.0.1,1,192.168.0.2,1,1,50

おわりに

Sliceable Switch を動作させ、同一スライスに所属するホスト同士のみが通信できることを確認してみました。今回はスライスの設定は、 slice コマンドを用いて行いましたが、REST API 経由でもできるようなしくみが用意されています。次回は REST API を使う方法について紹介したいと思います。