P4Runtimeでコントロールプレーンを開発する Python編 (1/2)

はじめに

前回の記事はP4言語の書き方,すなわちデータプレーンをどうやってプログラミングするかについて調べました*1
madomadox.hatenablog.com

今回は,コントロールプレーンをプログラミングするための,P4RuntimeというAPIについて調べました.
そして,P4Runtimeは現在GoとPythonで利用できるようでしたので,Pythonでコントロールプレーンをプログラミングしてみようと思います.
Python編と銘打っていますが,Goでの予定はございません.

本記事は,P4Runtime Specificationのversion 1.3.0を読んでまとめています.
https://p4.org/p4-spec/p4runtime/main/P4Runtime-Spec.pdf

また,P4言語のコンパイラはp4.orgによるp4c,ターゲットはbmv2,アーキテクチャはv1modelを利用したものであることを前提とします.

参考記事・参考文献

qiita.com
qiita.com
nextpublishing.jp

誤りがありましたら,ご指摘いただけますと幸いです.可及的速やかに修正いたします.

事前知識

P4Runtime

P4Runtimeは,ベンダーに依存せずに,共通のプラットフォームでP4エンティティ(P4言語で定義されたテーブルなど)を操作するためのAPIです.
P4Runtime Specificationにその仕様が定義されています.

P4のバイス(データプレーン)となるターゲットは1台以上のコントローラ(コントロールプレーン)から制御されます.
コントローラが複数ある場合は,その中から代表者のコントローラが選ばれます.代表者はメインコントローラ,その他大勢はスレーブコントローラと呼ばれます.
そしてコントローラがデバイスを制御するためのAPIの1つがP4Runtimeです.

P4Runtimeではコントローラがデバイスを制御するために,gRPCと呼ばれる技術を用いて通信します.
バイス内にはgRPCサーバが,そしてコントローラ内にはgRPCクライアントが実装されています.
バイスがコントローラのAPIを受け付けており,それに応じた処理をします.
そのため,デバイス内で動いているものは,P4Runtimeサーバということになります*2

Protocol BuffersとgRPC

P4RuntimeはProtobuf(Protocol Buffers)と呼ばれる形式で表現されたメッセージ定義によって提供されています.

Protocol Buffersとは,XMLJSONなどと同じく,データの構造を表現するフォーマットの1つです.

Protocol Buffersはスキーマ言語と呼ばれます.スキーマ言語ではデータの型やデータ同士の関係性などが定義できます.
このフォーマットは,gRPCというGoogleの開発したRPC(遠隔リモートプロシージャ)フレームワークに利用されています.ファイルの拡張子には.protoが利用されます.

protocという.protoファイル用のコンパイラプログラムがあり,これでprotoファイルをコンパイルすると,gRPC処理に関連するクラスが自動的に作られます.
サーバ側のクラスをServicer,クライアント側のクラスをStubと呼びます.
この雛形はGo,C++PythonJavaなど多岐にわたる言語で作成できるため,特定の言語に依存せずにgRPCを用いたアプリケーションを作成できる利点があります.

P4Runtimeで利用されるデータは,p4runtime.protoというProtocol Buffersファイルであらかじめ定義されています.
P4Runtimeでは,データプレーン側にgRPCサーバが,コントロールプレーン側にgRPCクライアントがあることになっているため,「コントローラプレーンを実装する」=「gRPCサーバにリクエストするgRPCクライアントを実装する」になります.
なお,データプレーンを自作しない限りは,データプレーン側で動作するgRPCサーバの処理はすでに実装済みであることを前提とします.

今回はP4RuntimeをPythonで書く予定ですので,PythonでのgRPCの例を出します.
.protoファイルをコンパイルすると,Protocol Buffersで記述したメッセージに関するクラスの定義があるxxxx_pb2.pyと,gRPCの通信に関するクラスの定義があるxxxx_pb2_grpc.pyが生成されます.
xxxx_pb2_grpc.pyからクライアント・サーバのクラスをインポートして利用(あるいは実装)し,やりとりするメッセージのクラスはxxxx_pb2.pyからインポートして利用します.

コントロールプレーンプログラミングの手順とPythonでのプログラミング

P4Runtimeを用いて,コントロールプレーンからデータプレーンを制御する際は,以下の手順に従います.

  1. コントローラとデバイス間の通信セッション確立
    • 必要な情報:MasterArbitrationUpdateメッセージ
    • 利用するRPC:StreamChannel
  2. コントローラからデバイスコンフィグとメタデータをデバイスにインストール
    • バイスコンフィグをデバイスにインストール
      • 必要な情報:SetForwardPipelineConfigRequestメッセージ
      • 利用するRPC:SetForwardPipelineConfig
    • 現在のデバイスコンフィグを確認
      • 必要な情報:GetForwardPipelineConfigRequestメッセージ
      • 利用するRPC:GetForwardPipelineConfig
  3. コントローラからデバイスを制御
    • エンティティの更新処理
      • 必要な情報:WriteRequestメッセージ
      • 利用するRPC:Write
    • エンティティの情報取得
      • 必要な情報:ReadRequestメッセージ
      • 利用するRPC:Read

それぞれ,gRPCクライアントから,デバイス上のgRPCサーバに対してリクエストを送信して実現します.
ここで,gRPCプロトコルの観点から見れば,データプレーンはgRPCサーバコントロールプレーンはgRPCクライアントの役割を持っています.
繰り返しになりますが,コントロールプレーンで実装するのはgRPCクライアントです.

今回はPythonでコントロールプレーンを作っていこうと思っていたのですが,実はすでにtutorials用にPythonのライブラリ?が作られていました.
github.com

本記事では,単に必要最低限の処理を理解することに焦点をあてて,下記のtutorialsのプログラムも参考にしつつ,より簡易的なプログラムでコントローラを作ることにします.
このtutorialsのリポジトリのライセンスはApache 2.0 Licenseです.
したがって,これ以降,私が記載したソースコードには,Apache 2.0 Licenseで配布されている成果物を一部利用したものがが含まれる箇所があります.

また,WriteとReadに関しては,実際にこのデータプレーンを配置したネットワーク環境で実験をする必要があることや,ソースコードの量も多くなることから,後半に分けています.
したがって,今回は,1.と2.までを実装します.

実験のための環境構築

実装をしていくにあたって,実際にgrpcサーバが稼動するデバイスを構築していこうと思います.
今回のターゲットはbmv2ですが,grpcが動くスイッチはswitch_simple_grpcとして用意されています.

ソースからコンパイルをしようとしたのですが,めちゃくちゃ手間取ってしまったので,下記ページにありました仮想マシンを利用させていただきました.
ユーザ名などの詳細は下記ページのREADME.mdをご確認ください.
github.com
ovaファイルとして提供されているので,VirtualBoxなどでインポートすることで仮想マシンを作成できます.
下記からは,この仮想マシン上にログインした環境でプログラミングをすることを前提とします.

なお,仮想マシンではp4runtime関連の.protoファイルがすでにコンパイルされており,p4runtime_pb2.py,p4runtime_pb2_grpc.pyなどがdist-packagesにインストールされています.
おそらく,これはP4RuntimeのPythonパッケージをpipでインストールした感じだと思いますので,実際はこれをインストールすれば,コントローラのプログラミングができる環境は構築できると思います.

>> import p4
>> p4.__file__
/usr/lib/python3/dist-packages/p4

そして今回は,tutorialsリポジトリのbasic.p4をデータプレーンのソースコードとして前提とすることにします.
github.com

まずは,はじめにgRPCのクライアントの基になるクラスを作ってみようと思います.
これに1.〜3.の内容を付け足していきます.
grpc.insecure_channel()でgRPCの通信チャネルを確立し,p4runtime_pb2_grpc.P4RuntimeStub()でリクエストを送ります.
xxx_grpcに定義されているXXXStubクラスを使ってリクエストを送信します.

from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

1. コントローラとデバイス間の通信セッション確立

説明

コントローラとデバイスがセッションを確立するには,今のコントローラがメインコントローラとして選ばれる必要があります.
コントローラはdevice_idとrole_idという値の組で区別され,同じ組は,同じデバイスに対して同じ役割を持つコントローラ群と言えます.
各コントローラはelection_idを持っており,これが最も高いものがメインのコントローラとして選ばれます.
role_idとelection_idはコントローラが決めるもので,デバイス側ではタッチしません.

メインコントローラとして選ばれるためには,StreamMessageRequestというメッセージを作り,gRPCでデバイスに送信します.

ここからは,p4/v1/p4runtime.protoを見ながら,それぞれ構成を確認してみます.
github.com

まずは,rpcと書かれている箇所を読んでみようと思います.
ここがサービスの機能となる箇所です.

  // Represents the bidirectional stream between the controller and the
  // switch (initiated by the controller), and is managed for the following
  // purposes:
  // - connection initiation through client arbitration
  // - indicating switch session liveness: the session is live when switch
  //   sends a positive client arbitration update to the controller, and is
  //   considered dead when either the stream breaks or the switch sends a
  //   negative update for client arbitration
  // - the controller sending/receiving packets to/from the switch
  // - streaming of notifications from the switch
  rpc StreamChannel(stream StreamMessageRequest)
      returns (stream StreamMessageResponse) {
  }

これを見ると,どうやらStreamMessageRequestメッセージを利用するStreamChannelで接続の開始ができるようです.

  • connection initiation through client arbitration

次に,必要となるメッセージを見てみます.

message StreamMessageRequest {
  oneof update {
    MasterArbitrationUpdate arbitration = 1;
    PacketOut packet = 2;
    DigestListAck digest_ack = 3;
    .google.protobuf.Any other = 4;
  }
}

oneofと付いているので,updateは{}の中のうちどれか1つのメッセージが含まれるということが分かります.
今回使うものは,MasterArbitrationUpdateというメッセージです.
MasterArbitrationUpdateには次のような情報が含まれます.

message MasterArbitrationUpdate {
  uint64 device_id = 1;


  // The role for which the primary client is being arbitrated. For use-cases
  // where multiple roles are not needed, the controller can leave this unset,
  // implying default role and full pipeline access.
  Role role = 2;


  // The stream RPC with the highest election_id is the primary. The 'primary'
  // controller instance populates this with its latest election_id. Switch
  // populates with the highest election ID it has received from all connected
  // controllers.
  Uint128 election_id = 3;


  // Switch populates this with OK for the client that is the primary, and
  // with an error status for all other connected clients (at every primary
  // client change). The controller does not populate this field.
  .google.rpc.Status status = 4;
}

コントロールに複数のroleが必要ない場合は,roleを省略しても良いとあります.
また,statusもデバイス側が結果を返すもので,コントローラが入力するものではないともあります.

あとセットするものはelection_idです.election_idはhighとlowを持っています.
election_idが全体で128ビットあるうち,上位64ビットと下位64ビットを別々に指定します.
今回は特に理由はありませんが,00000(中略)000001という値にしようと思うので,high=0, low=1としようと思います.

message Uint128 {
  // Highest 64 bits of a 128 bit number.
  uint64 high = 1;
  // Lowest 64 bits of a 128 bit number.
  uint64 low = 2;
}
実装

上記を踏まえて,pythonプログラムで,StreamMessageRequestを構築して,StreamChannelで接続を確立します.
pythonでのgRPCに詳しくないのですが,stream系のRPCはイテレータ形式でリクエストを渡す必要があるようです.
qiita.com

といったところで,当初は下記のような書き方をしてみたのですが,その後後述するSetForwardingPipelineConfigでエラーが発生しました('Not primary'というエラーが表示されました).

    # New!!
    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # イテレータ形式で送信
        message = []
        message.append(request)
        for resp in self.client_stub.StreamChannel(iter(message)):
            print(resp)

p4/v1/p4runtime.protoを確認すると,streamが終了するとセッションが切れますと書いてあるので,StreamChannelのrequestが消費されるとセッションが終了してしまい,後続のSetForwardingPipelineConfigが別のセッションでの通信と扱われてしまうという感じでしょうか?

indicating switch session liveness: the session is live when switch
// sends a positive client arbitration update to the controller, and is
// considered dead when either the stream breaks or the switch sends a
// negative update for client arbitration

これについては,下記でも説明がありました.どうやらイテレート可能なQueueを使ってこれを回避しているようでした.
github.com

今回,この箇所についてはtutorialsリポジトリで公開されているライブラリのコードを引用させていただきました.
StreamChannelの引数にQueueを与えておくと非同期で処理を待機させることができ,リクエストがpushされると,そのリクエストをStreamChannelで送出するという感じでしょうか?
PythonのgRPCライブラリの都合なのか,P4Runtimeの仕様的な都合なのか,いまいちこのあたりを使い慣れていないのでよく分かりませんが,詳しい方がいらっしゃれば教えていただきたいところです.

ということで,最終的なコードとしては下記のようになります.
(ライセンス表記について問題があれば,ご指摘いただけますと幸いです.)

from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc

from queue import Queue # <== New!!
"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
# New!!
class IterableQueue(Queue): 
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue() # <== New !!
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))# <== New!!


    # New!!
    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # キューにpush
        self.request_queue.put(request)
        for item in self.request_stream:
            return item
結果

結果を確認する前に,データプレーンとなるターゲットを起動します.
bmv2の場合は,simple_switch_grpcコマンドを実行します.その際,device_idを100に指定しておきます.

$ simple_switch_grpc basic.json --device-id 100
Calling target program-options parser
Server listening on 0.0.0.0:9559

すると,上記のようにポート番号9559でリクエストを待ち受けるようになります.
P4Runtimeのポート番号が,IANAで9559が指定されているようですね.
https://p4.org/p4-spec/p4runtime/main/P4Runtime-Spec.html

The server must listen on TCP port 9559 by default, which is the port that has been allocated by IANA for the P4Runtime service.

ターゲットを起動できたので,コントロールプレーンのプログラムを動かしてみます.

下記のコード(「実装」節で書いたコードにmainとなる処理を記載したもの)で結果を確認します.

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    resp = controller.MasterArbitrationUpdate()
    print(resp)
$ python3 mycontroller.py

arbitration {
  device_id: 100
  election_id {
    low: 1
  }
  status {
    message: "Is primary"
  }
}

statusを見ると,"Is primary"と返ってきているので,無事にプライマリとして選出されたことがわかります.

2. コントローラからデバイスコンフィグをデバイスにインストール

SetForwardingPipelineConfig

説明

SetForwardingPipelineConfigを使うと,デバイスに設定をインストールできます.
これは,p4runtime.protoを見てみると,以下のrpcで実現できることがわかります.

// Sets the P4 forwarding-pipeline config.
rpc SetForwardingPipelineConfig(SetForwardingPipelineConfigRequest)
    returns (SetForwardingPipelineConfigResponse) {
}

このrpcの引数を見るとSetForwardingPipelineConfigRequestとあるので,これを送れば良いことが分かります.
さっそく,その構造をp4/v1/p4runtime.protoで確認してみます.

message SetForwardingPipelineConfigRequest {
  uint64 device_id = 1;
  uint64 role_id = 2 [deprecated=true];
  string role = 6;
  Uint128 election_id = 3;
  Action action = 4;
  ForwardingPipelineConfig config = 5;
}

roleは省略して良くて,device_idやelection_idも1. と同じように設定できます.

しかし,Actionが分かりませんので,そちらも読んでみます.
実態となるデータはenum型のようです.
以下を読んでみると,設定を保存してそれを反映させたいので,VERIFY_AND_COMMITを使えば良いのかなぁというのが分かります.

message SetForwardingPipelineConfigRequest
  enum Action {
    UNSPECIFIED = 0;


    // Verify that the target can realize the given config. Do not modify the
    // forwarding state in the target. Returns error if config is not provided
    // of if the provided config cannot be realized.
    VERIFY = 1;


    // Save the config if the target can realize it. Do not modify the
    // forwarding state in the target. Any subsequent read/write requests must
    // refer to fields in the new config. Returns error if config is not
    // provided of if the provided config cannot be realized.
    VERIFY_AND_SAVE = 2;


    // Verify, save and realize the given config. Clear the forwarding state
    // in the target. Returns error if config is not provided of if the
    // provided config cannot be realized.
    VERIFY_AND_COMMIT = 3;


    // Realize the last saved, but not yet committed, config. Update the
    // forwarding state in the target by replaying the write requests since the
    // last config was saved. Config should not be provided for this action
    // type. Returns an error if no saved config is found or if a config is
    // provided with this message.
    COMMIT = 4;


    // Verify, save and realize the given config, while preserving the
    // forwarding state in the target. This is an advanced use case to enable
    // changes to the P4 forwarding pipeline configuration with minimal traffic
    // loss. P4Runtime does not impose any constraints on the duration of the
    // traffic loss. The support for this option is not expected to be uniform
    // across all P4Runtime targets. A target that does not support this option
    // may return an UNIMPLEMENTED error. For targets that support this option,
    // an INVALID_ARGUMENT error is returned if no config is provided, or if
    // the existing forwarding state cannot be preserved for the given config
    // by the target.
    RECONCILE_AND_COMMIT = 5;
  }

続いて,ForwardingPipelineConfigについても確認してみます.
p4info,p4_device_configについては次節に飛ばすとして,Cookieはここでいま作成しているConfigを区別できれば何でも良いようですので,適当に決められそうです.

message ForwardingPipelineConfig {
  config.v1.P4Info p4info = 1;


  // Target-specific P4 configuration.
  bytes p4_device_config = 2;


  // Metadata (cookie) opaque to the target. A control plane may use this field
  // to uniquely identify this config. There are no restrictions on how such
  // value is computed, or where this is stored on the target, as long as it is
  // returned with a GetForwardingPipelineConfig RPC. When reading the cookie,
  // we need to distinguish those cases where a cookie is NOT present (e.g. not
  // set in the SetForwardingPipelineConfigRequest, therefore we wrap the actual
  // uint64 value in a protobuf message.
  message Cookie {
    uint64 cookie = 1;
  }
  Cookie cookie = 3;
}
P4Infoについて

ここからは,先程飛ばしたもののうちの一つ,p4infoについて書いていきます.
p4infoは,P4Infoフォーマットのmessageを指していて,テーブルやアクションなどのP4に関するオブジェクト(P4エンティティ)に関する情報が格納されています.
各エンティティにはIDが振られています.

このフォーマットはどう作るかと言うと,bmv2の場合ではp4cコマンドでオプションを指定すると,一緒に作られます.
実際にP4Infoファイルを作って中身を見てみます.
ここでは,下記のbasic.p4に関するP4Infoファイルを作成します.

P4言語で書いたソースコードを通常のp4cコマンドでコンパイルすると,basic.p4iファイルとbasic.jsonファイルが生成されます.
.p4iファイルは,プリプロセッサが出力したP4ファイル,.jsonがBMv2モデルのsimple_switch_xxxxのプログラムを実行するのに必要なJSONファイルが生成されます.
github.com

a file with suffix .p4i, which is the output from running the preprocessor on your P4 program.
a file with suffix .json that is the JSON file format expected by BMv2 behavioral model simple_switch.

このコマンドを,--p4runtime-filesオプションを付けて実行すると,P4Info用のファイルを生成できます.

$ p4c basic.p4 --p4runtime-files basic.p4info
$ ls
basic.json  basic.p4  basic.p4i  basic.p4info

basic.p4infoファイルの中身を見てみます.

pkg_info {
  arch: "v1model"
}
tables {
  preamble {
    id: 37375156
    name: "MyIngress.ipv4_lpm"
    alias: "ipv4_lpm"
  }
  match_fields {
    id: 1
    name: "hdr.ipv4.dstAddr"
    bitwidth: 32
    match_type: LPM
  }
   (中略)
}
actions {
  preamble {
    id: 21257015
    name: "NoAction"
    alias: "NoAction"
    annotations: "@noWarn(\"unused\")"
  }
}
actions {
  preamble {
    id: 25652968
    name: "MyIngress.drop"
    alias: "drop"
  }
}
actions {
  preamble {
    id: 28792405
    name: "MyIngress.ipv4_forward"
    alias: "ipv4_forward"
  }
  params {
    id: 1
    name: "dstAddr"
    bitwidth: 48
  }
  params {
    id: 2
    name: "port"
    bitwidth: 9
  }
}
type_info {
}

これを見ると,アーキテクチャや,定義したテーブル,アクションの情報が格納されてることがわかります.
テーブルやアクションにIDが振られていることも確認できると思います.

どんな情報が含まれるかについては,p4/config/p4info.protoに定義されています.p4info.protoから引用(コメントは本記事の著者が削除,一部中略)すると,
github.com

message P4Info {
  PkgInfo pkg_info = 1;
  repeated Table tables = 2;
  repeated Action actions = 3;
  repeated ActionProfile action_profiles = 4;
  repeated Counter counters = 5;
  repeated DirectCounter direct_counters = 6;
  repeated Meter meters = 7;
  repeated DirectMeter direct_meters = 8;
  repeated ControllerPacketMetadata controller_packet_metadata = 9;
  repeated ValueSet value_sets = 10;
  repeated Register registers = 11;
  repeated Digest digests = 12;
  repeated Extern externs = 100;
  P4TypeInfo type_info = 200;
}
     :
    (中略)
     :

message Table {
  Preamble preamble = 1;
  repeated MatchField match_fields = 2;
  repeated ActionRef action_refs = 3;
  uint32 const_default_action_id = 4;
  uint32 implementation_id = 6;
  repeated uint32 direct_resource_ids = 7;
  int64 size = 8;  // max number of entries in table
  enum IdleTimeoutBehavior {
    NO_TIMEOUT = 0;
    NOTIFY_CONTROL = 1;
  }
  IdleTimeoutBehavior idle_timeout_behavior = 9;
  bool is_const_table = 10;
  google.protobuf.Any other_properties = 100;
}
      :
     (中略)
      :
message Action {
  Preamble preamble = 1;
  message Param {
    uint32 id = 1;
    string name = 2;
    repeated string annotations = 3;
    repeated SourceLocation annotation_locations = 8;
    int32 bitwidth = 4;
    Documentation doc = 5;
    P4NamedType type_name = 6;
    repeated StructuredAnnotation structured_annotations = 7;
  }
  repeated Param params = 2;
}

であり,実際に生成されたP4Infoファイルには,上記の情報が含まれていることがわかります.
照らし合わせてみた例が下記のものです.

[P4infoファイル(basic.p4info)]

tables {
  preamble {
    id: 37375156
    name: "MyIngress.ipv4_lpm"
    alias: "ipv4_lpm"
  }
  match_fields {
    id: 1
    name: "hdr.ipv4.dstAddr"
    bitwidth: 32
    match_type: LPM
  }
  action_refs {
    id: 28792405
  }
}
[p4info.proto]

message Table {
  Preamble preamble = 1;
  repeated MatchField match_fields = 2;
  repeated ActionRef action_refs = 3;
  (中略)

出力されたp4infoのファイルはフォーマットに沿っているので,ファイルの文字列を読み込んでgoogle.protobuf.text_format.Merge()でmessageに変換することができます.
google.protobuf.text_format.Merge()では新しくmessageのクラスの変数を作成します.
これを直接代入しようとするとエラーが出るので,CopyFrom()を使って値を更新します.

p4info = p4info_pb2.P4Info()
with open(p4info_file, "r") as f:
    google.protobuf.text_format.Merge(f.read(), p4info)

request.config.p4info.CopyFrom(p4info)
p4_device_configについて

p4_device_configは,ターゲットに関する情報で,bmv2の場合は,p4cで作成された.jsonファイルがそれに当たります.
p4_device_configの型はbytesなので,.jsonファイルをバイナリ形式で読み出して設定します.

device_config = None
with open(p4_device_config_file, "rb") as f:
    device_config = f.read()
request.config.p4_device_config = device_config
実装

最後に,上記を踏まえてプログラムを書いていきます.
p4infoに関するクラスが必要なので,p4.config.v1からp4info_pb2をimportします.

from p4.config.v1 improt p4info_pb2 # <== New!!
from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc
import google.protobuf.text_format # <== New!!

from queue import Queue

"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
class IterableQueue(Queue):
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue()
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))

    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        # キューにpush
        self.request_queue.put(request)
        for item in self.request_stream:
            return item

    #New!!
    def SetPipelineConfigForward(self, p4info_file, p4_device_config_file): 
        request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
        request.election_id.high = 0
        request.election_id.low = 1
        request.device_id = self.device_id

        # p4infoに関する情報をmessageとして取得
        p4info = p4info_pb2.P4Info()
        with open(p4info_file, "r") as f:
            google.protobuf.text_format.Merge(f.read(), p4info)

        # device_configをbytesとして取得
        # jsonファイルをバイナリ形式で読み込む
        device_config = None
        with open(p4_device_config_file, "rb") as f:
            device_config = f.read()

        request.config.p4info.CopyFrom(p4info)
        request.config.p4_device_config = device_config
        request.config.cookie.cookie = 1
        request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
        resp = self.client_stub.SetForwardingPipelineConfig(request)
        return resp
結果

下記のコードで結果を確認します.(手を抜いてファイル名を直打ちで書いてます)

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    controller.MasterArbitrationUpdate()
    resp = controller.SetPipelineConfigForward(p4info_file="basic.p4info", p4_device_config_file="basic.json")
    print(resp)
$ python3 mycontroller.py

device_id: 100                                                                                                                                                                        
election_id {                                                                                                                                                                         
  low: 1                                                                                                                                                                              
}                                                                                                                                                                                     
action: VERIFY_AND_COMMIT                                                                                                                                                             
config {                                                                                                                                                                              
  p4info {
          (.p4infoの内容が記載されている.中略)                                                                                                                                                                            
   }
  p4_device_config: "(.jsonの内容が記載されている.中略)"
  cookie {
    cookie: 1
  }
}

自分が設定した内容が返ってくるだけですが,結果を確認することができました.

GetForwardPipelineConfig

説明

SetForwardingPipelineConfigが終わったので,今度はForwardGetPipelineConfigを使って,現在の設定を確認してみます.
p4runtime.protoを確認してみると,GetForwardingPipelineConfigRequestというmessageが必要なようです.

  // Gets the current P4 forwarding-pipeline config.
  rpc GetForwardingPipelineConfig(GetForwardingPipelineConfigRequest)
      returns (GetForwardingPipelineConfigResponse) {
  }

GetForwardingPipelineConfigRequestを見てみると,ResponseTypeはデフォルトでALLが選択されるようなので,device_idだけ設定すれば良さそうです.

message GetForwardingPipelineConfigRequest {
  // Specifies the fields to populate in the response.
  enum ResponseType {
    // Default behaviour. Returns a ForwardingPipelineConfig with all fields set
    // as stored by the target.
    ALL = 0;
    // Reply by setting only the cookie field, omitting all other fields.
    COOKIE_ONLY = 1;
    // Reply by setting the p4info and cookie fields.
    P4INFO_AND_COOKIE = 2;
    // Reply by setting the p4_device_config and cookie fields.
    DEVICE_CONFIG_AND_COOKIE = 3;
  }
  uint64 device_id = 1;
  ResponseType response_type = 2;
}
実装

ここは単純に,GetForwardingPipelineConfigRequestについてdevice_idだけ設定すれば送れば良さそうです.

from p4.config.v1 improt p4info_pb2
from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc
import grpc
import google.protobuf.text_format

from queue import Queue

"""
    This IterableQueue function is ... 
    source:  https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/switch.py
    license: Apache 2.0, https://github.com/p4lang/tutorials/blob/master/LICENSE

"""
class IterableQueue(Queue):
    _sentinel = object()
    
    def __iter__(self):
        return(self.get(), self._sentinel)

    def close(self):
        self.put(self._sentinel)

class MyController():
    def __init__(self, address, port, device_id):
        self.device_address = address + ":" + port
        self.device_id = device_id

        self.channel = grpc.insecure_channel(self.address)
        self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel)

        self.request_queue = IterableQueue()
        self.request_stream = self.client_stub.StreamChannel(iter(self.request_queue))

    def MasterArbitrationUpdate(self):
        request = p4runtime_pb2.StreamMessageRequest()
        request.arbitration.device_id = self.device_id
        request.arbitration.election_id.high = 0
        request.arbitration.election_id.low = 1

        self.request_queue.put(request)
        for item in self.request_stream:
            return item


    def SetPipelineConfigForward(self, p4info_file, p4_device_config_file): 
        request = p4runtime_pb2.SetForwardingPipelineConfigRequest()
        request.election_id.high = 0
        request.election_id.low = 1
        request.device_id = self.device_id

        p4info = p4info_pb2.P4Info()
        with open(p4info_file, "r") as f:
            google.protobuf.text_format.Merge(f.read(), p4info)


        device_config = None
        with open(p4_device_config_file, "rb") as f:
            device_config = f.read()

        request.config.p4info.CopyFrom(p4info)
        request.config.p4_device_config = device_config
        request.config.cookie.cookie = 1
        request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT
        resp = self.client_stub.SetForwardingPipelineConfig(request)
        return resp

    # New!!
    def GetPipelineConfigForward(self):
        request = p4runtime_pb2.GetForwardingPipelineConfigRequest()
        request.device_id=self.device_id
        resp = self.client_stub.GetForwardingPipelineConfig(request)
        return resp
結果

SetForwardingPipelineConfigのレスポンスと同じ内容が見えます.

(中略)
if __name__ == "__main__":
    controller = MyController(
                    address="127.0.0.1",
                    port="9559",
                    device_id=100
                )
    controller.MasterArbitrationUpdate()
    controller.SetPipelineConfigForward(p4info_file="basic.p4info", p4_device_config_file="basic.json")
    resp = controller.GetPipelineConfigForward()
    print(resp)
$ python3 mycontroller.py

device_id: 100                                                                                                                                                                        
election_id {                                                                                                                                                                         
  low: 1                                                                                                                                                                              
}                                                                                                                                                                                     
action: VERIFY_AND_COMMIT                                                                                                                                                             
config {                                                                                                                                                                              
  p4info {
          (.p4infoの内容が記載されている.中略)                                                                                                                                                                            
   }
  p4_device_config: "(.jsonの内容が記載されている.中略)"
  cookie {
    cookie: 1
  }
}

まとめ

P4Runtimeは共通のプラットフォームでP4エンティティを操作するためのAPIとして仕様化されています.
通信方式はgRPCを前提としており,Protocol Buffersというフォーマットで定義されたデータを用いて通信します.
P4Runtimeに関するデータはProtocol Buffersで定義されており,こちらを利用します.

P4RuntimeはGo言語とPython言語で提供されており,今回はPythonでコントロールプレーンのプログラミングをしました.
ターゲットの制御にあたっては,(1)MasterArbitrationUpdateでメインコントローラとして選出され,(2)SetForwardingPipelineConfigでターゲットに設定をインストールし,(3)Writeでデバイス上のP4エンティティの内容を更新します.
simple_switch_grpcという,gRPCが動くbmv2ターゲットを起動して,(1)と(2)について,実際にレスポンスが返ってくることを確認しました.

Write,Readなど,実際に本質的な制御に利用する箇所(3)については後半で書いていこうと思います.

*1:疲れてはやめて,飽きてはやめてで書き溜めていたところ,いつの間にか前回の記事から1年ほど経ちました.

*2:コントローラはローカル環境かリモート環境かに置かれます.ローカルの場合はP4デバイスの中にデータプレーンとコントロールプレーンが同居しており,リモートの場合はデータプレーンの外にコントロールプレーンが置かれます.