P4Runtime を用いて P4 で記述した簡易 L2 Switch のテーブルエントリ登録とトラヒックカウンタ値取得を行う（準備編）


# はじめに

[前回の記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)では P4 で記述した簡易 L2 Switch に VLAN (802.1q) 対応，およびトラヒックカウンタ実装を行いました． 本記事では，前回実装した簡易 L2 Switch を [P4Runtime](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html) で制御し，MACテーブルのエントリ登録，およびカウンタ値の取得を行う P4Runtime client を実装して， P4Runtime を用いたコントロールプレーン開発の基礎について説明します．なお，言語は Go 言語を用いて実装します．

Go を用いた P4Runtime client については既に実装・公開されているものもあり，例えば[ ebiken さんの github ](https://github.com/ebiken/p4labs)や[ antonin さんの github ](https://github.com/antoninbas/p4runtime-go-client) が大変参考になります．本記事と併せて参照すれば相乗効果が高いと思います．

### 本記事のサマリ

本記事には P4Runtime を用いてコントロールプレーンを開発するために必要な要素として，下記の内容が記載されています．

- P4Runtime の概要（コントロールプレーン実装の基礎）
- テーブルエントリの登録方法
- マルチキャストグループの登録方法
- トラヒックカウンタ（Counter）値の取得方法

P4Runtime を用いたコントロールプレーン実装の基礎を「準備編（本記事）」で， P4Runtime の Go 実装を用いたテーブルエントリ登録やトラヒックカウンタ値の取得を「[実装編](https://qiita.com/13ryuse4/items/3a68a79e81f621b85dd5)」でそれぞれ説明します．そもそも P4 って何？という方は[前回の記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)を参照頂ければと思います．

### おことわり

本記事は 2020.12 時点の P4Runtime Specification を参照した説明となっています． P4Runtime は標準化作業中の内容であるため，最新版の P4Runtime Specification を[公式ページ](https://p4.org/specs/)で参照するようお願いいたします．

# P4Runtime によるコントロールプレーン実装の基礎

準備編では，そもそも P4Runtime とは何か， を起点とし P4Runtime を用いたコントールプレーン開発の要素として，大きく下記３つについて概説します．

- StreamChannel と Primary 選出
    - コントロールプレーンとデータプレーンの接続処理を行います．
- ForwardingPipelineConfig の設定
    - P4 で記述した自作データプレーンを target（P4対応デバイス）に設定します．
- Entity の Write/Read
    - Entity（データプレーンの制御可能なオブジェクト）の読み書きを行います．

## P4Runtime とは

P4Runtime とは [P4.org](https://p4.org/specs/) にて仕様化が進められている P4 のコントロールプレーン向けの制御インターフェースです． [protocol buffers](https://developers.google.com/protocol-buffers) を用いて記述されており [p4lang の github](https://github.com/p4lang/PI/tree/master/proto) で公開されています． 複数の proto ファイルが存在しますがメインとなるのは [p4runtime.proto](https://github.com/p4lang/p4runtime/blob/master/proto/p4/v1/p4runtime.proto) で，特に P4Runtime の client（コントロールプレーン）は下記のように定義されています（コメントを一部省略しています）．

```protobuf

service P4Runtime {
  // Update one or more P4 entities on the target.
  rpc Write(WriteRequest) returns (WriteResponse) {
  }
  // Read one or more P4 entities from the target.
  rpc Read(ReadRequest) returns (stream ReadResponse) {
  }

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

  // Represents the bidirectional stream between the controller and the
  // switch (initiated by the controller) ...
  rpc StreamChannel(stream StreamMessageRequest)
      returns (stream StreamMessageResponse) {
  }

  rpc Capabilities(CapabilitiesRequest) returns (CapabilitiesResponse) {
  }
}
```

P4Runtime を用いたコントロールプレーンは，上記で定義される RPC (Remote Procedure Call) をベースに [gRPC](https://grpc.io/) client として実装されます． それぞれの RPC の役割はざっくりと下記のようになります．

- ```StreamChannel``` RPC
    - コントロールプレーンとデータプレーン間で双方向のセッションを確立します．
- ```SetForwardingPipelineConfig``` RPC
    - データプレーンに対し forwarding-pipeline config を設定します．
- ```GetForwardingPipelineConfig``` RPC
    - データプレーンから forwarding-pipeline config を取得します．
- ```Write``` RPC
    - P4 Entity (P4Runtime で制御可能なオブジェクト） の update を行います．
- ```Read``` RPC
    - P4 Entity (P4Runtime で制御可能なオブジェクト） の read を行います．

Capabilities については本記事では割愛しますが， p4runtime.proto で定義される ```CapabilitiesRequest/Response```（下記）より推測される限り， データプレーン側が対応している P4Runtime のバージョンを確認する RPC のようです．

```protobuf:
message CapabilitiesRequest {
}

message CapabilitiesResponse {
  // The full semantic version string (e.g. "1.1.0-rc.1") corresponding to the
  // version of the P4Runtime API currently implemented by the server.
  string p4runtime_api_version = 1;
}
```

P4Runtime に対応した target には gRPC server が用意されており，コントロールプレーン（gRPC client）と target（上の gRPC server) が上記の RPC で通信し，テーブルエントリの登録やトラヒックカウンタ値の取得等を行います．

## StreamChannel と Primary 選出

P4Runtime でデータプレーンを制御するためには，まずコントロールプレーンとデータプレーン間にセッションを確立する必要があります． ```StreamChannel``` RPC はこのセッション確立を行うための RPC で [specification](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html#sec-client-arbitration-and-controller-replication) には下記のように記載されています．

> To support multiple controllers, P4Runtime uses the streaming channel (available via StreamChannel RPC) for session management. The workflow is described as follows:

> - Each controller instance (e.g. a controller process) can participate in one or more roles. For each (device_id, role_id), the controller receives an election_id. This election_id can be the same for different roles and/or devices, as long as the tuple (device_id, role_id, election_id) is unique. For each (device_id, role_id) that the controller wishes to control, it establishes a StreamChannel with the P4Runtime server responsible for that device, and sends a MasterArbitrationUpdate message containing that tuple of (device_id, role_id, election_id) values. The P4Runtime server selects a primary independently for each (device_id, role_id) pair.  <以下略>

詳細は前後の文脈を参照頂きたいですが，ざっくりと下記のような意味です．

P4Runtime は複数のコントロールプレーンの接続を許容するために ```device_id``` ```role_id``` ```election_id``` の 3 つのパラメータを用いてコントロールプレーンプロセスを識別します． 特に ```device_id``` と ```role_id``` の組み合わせ毎に ```election_id``` が最大となるコントロールプレーンを Primary に選出します． Primary となったコントロールプレーンは ```role_id``` に紐付く "役割" の範囲内でデータプレーンの排他的な制御が可能となります．

なお ```role_id``` に紐付く "役割" をどのように規定するかは P4Runtime の対象外となります．また ```role_id``` を指定しない場合は "full pipeline access" を表す [default role](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html#sec-default-role) が設定され，その ```role_id``` は 0 となります．

コントロールプレーンは ```StreamChannel``` を介して ```MasterArbitrationUpdate``` message を送信し  ```device_id``` ```role_id``` ```election_id``` をデータプレーンに通知します．データプレーンは ```device_id``` ```role_id``` に対応するその時点での Master の ```election_id``` と ```MasterArbitrationUpdate``` の ```election_id``` を比較して Primary の選出を行います．

```MasterArbitrationUpdate``` は実際には ```StreamMessageRequest``` に含まれる形で ```StreamChannel``` RPC を介してデータプレーンに送信されます． 各 message は p4runtime.proto にてそれぞれ下記のように定義されています（一部コメントを省略しています）．

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

...

message MasterArbitrationUpdate {
  uint64 device_id = 1;
  Role role = 2;
  Uint128 election_id = 3;
  .google.rpc.Status status = 4;
}
```

```StreamMessageRequest``` に含まれる ```PacketOut``` や ```DigestListAck``` は本記事では割愛しますが，コントロールプレーンとの PacketI/O や [Digest](https://p4.org/p4-spec/docs/PSA-v1.1.0.html#sec-packet-digest) 利用時に用いるようです． こちらは [ebiken さんの github](https://github.com/ebiken/p4labs) で実装があるため興味のある方はこちらを参照ください．

このように P4Runtime でデータプレーンを制御するためには，まず P4Runtime client（コントロールプレーン）がデータプレーンとの間で ```StreamChannel``` を確立し Primary となる必要があります．

## ForwardingPipelineConfig の設定

コントロールプレーンとデータプレーンの間で ```StreamChannel``` を確立し Primary に選出されたら，次はデータプレーンそのものの設定を行う必要があります． データプレーンに関する設定は ```ForwardingPipelineConfig``` として p4runtime.proto で下記のように定義されています．

```protobuf:
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. ...
  message Cookie {
    uint64 cookie = 1;
  }
  Cookie cookie = 3;
}
```

```p4info``` はデータプレーンの各オブジェクト（table や action 等）に関する情報を管理する補助情報で [p4info.proto](https://github.com/p4lang/p4runtime/blob/master/proto/p4/config/v1/p4info.proto) で定義されています． 

```p4info``` および ```p4_device_config``` はいずれも P4 プログラムのコンパイル時に生成されます． コンパイラによって生成方法にばらつきがあるかと思いますが，例えば p4.org による [P4 コンパイラの reference 実装](https://github.com/p4lang/p4c) では下記のようにコンパイルを行うことで ```p4info.txt``` と ```my-p4-program.json``` が生成されます（```target``` や ```arch``` は環境に合わせて変更してください）．

```shell
p4c --target bmv2 --arch v1model --p4runtime-files p4info.txt my-p4-program.p4
```

コンパイラによって生成された ```p4info``` と ```p4_device_config``` を格納した ```ForwardingPipelineConfig``` より```SetForwardingPipelineConfig``` RPC でデータプレーンの設定を行います． 実際には下記で定義される ```SetForwardingPipelineConfigRequest``` message に ```ForwardingPipelineConfig``` を格納してデータプレーンに送信します（コメントを一部省略しています）．

```protobuf:
message SetForwardingPipelineConfigRequest {
  enum Action {
    UNSPECIFIED = 0;
    // Verify that the target can realize the given config. ...
    VERIFY = 1;
    // Save the config if the target can realize it. ...
    VERIFY_AND_SAVE = 2;
    // Verify, save and realize the given config. ...
    VERIFY_AND_COMMIT = 3;
    // Realize the last saved, but not yet committed, config. ...
    COMMIT = 4;
    // Verify, save and realize the given config, while preserving the
    // forwarding state in the target. ...
    RECONCILE_AND_COMMIT = 5;
  }
  uint64 device_id = 1;
  uint64 role_id = 2;
  Uint128 election_id = 3;
  Action action = 4;
  ForwardingPipelineConfig config = 5;
}
```
コントロールプレーンの識別のために ```device_id``` ```role_id``` ```election_id``` も同時に格納していますが，これらに加えて下記いずれかの ```action``` を選択します．

- VERIFY
    - ```ForwardingPipelineConfig``` が適用可能かの確認のみ．
- VERIFY_AND_SAVE
    - ```ForwardingPipelineConfig``` が適用可能かを確認した後に保存（適用はしない）．
- VERIFY_AND_COMMIT
    - ```ForwardingPipelineConfig``` が適用可能かを確認した後に適用．
- COMMIT
    - 最後に保存した ```ForwardingPipelineConfig``` を適用．
- RECONCILE_AND_COMMIT
    - データプレーンの状態を保護しつつ ```ForwardingPipelineConfig``` の確認と適用．

```ForwardingPipelineConfig``` を Commit まで実施することで，自作データプレーンを target にデプロイすることが出来ます．

## Entity の Write/Read

```StreamChannel``` を確立し ```ForwardingPipelineConfig``` の設定まで完了したら，Entity の Write/Read を通したデータプレーンの制御・データプレーンからの情報取得が可能となります． Entity とは P4Runtime で制御可能なオブジェクトの総称で p4runtime.proto では下記のように定義されています．

```protobuf

message Entity {
  oneof entity {
    ExternEntry extern_entry = 1;
    TableEntry table_entry = 2;
    ActionProfileMember action_profile_member = 3;
    ActionProfileGroup action_profile_group = 4;
    MeterEntry meter_entry = 5;
    DirectMeterEntry direct_meter_entry = 6;
    CounterEntry counter_entry = 7;
    DirectCounterEntry direct_counter_entry = 8;
    PacketReplicationEngineEntry packet_replication_engine_entry = 9;
    ValueSetEntry value_set_entry = 10;
    RegisterEntry register_entry = 11;
    DigestEntry digest_entry = 12;
  }
}
```

上記の proto ファイルより，テーブルエントリ（```TableEntry```）やトラヒックカウンタ（```CounterEntry```）も Entity として定義されていることが分かります．これらの Entity に対し ```Write``` RPC や ```Read``` RPC を通した制御あるいは情報取得を行うことで，コントロールプレーンによるデータプレーンの制御やデータプレーンからの情報取得が可能となります． なお，上記の Entity のうち本記事で取り上げるものは ```TableEntry``` ```PacketReplicationEngineEntry``` ```CounterEntry``` の３つになります．これらの Entity の詳細は「実装編（執筆予定）」で Go 言語による実装も加味しつつ掘り下げて説明します．

### Entity の Write

Entity への書き込み（テーブルエントリの登録等）は ```Write``` RPC を用いて行います． コントロールプレーン側では p4runtime.proto で定義される下記の ```WriteRequest``` message に必要な情報を入力し```Write``` RPC でデータプレーン側に通知します（下記の記載はコメントを一部省略しています）．

```protobuf

message WriteRequest {
  uint64 device_id = 1;
  uint64 role_id = 2;
  Uint128 election_id = 3;
  // The write batch, comprising a list of Update operations. ...
  repeated Update updates = 4;
  enum Atomicity {
    // Required. This is the default behavior. The batch is processed in a
    // non-atomic manner from a data plane point of view. ...
    CONTINUE_ON_ERROR = 0;
    // Optional. Operations within the batch are committed to data plane until
    // an error is encountered. At this point, the operations must be rolled
    // back such that both software and data plane state is consistent with the
    // state before the batch was attempted. ...
    ROLLBACK_ON_ERROR = 1;
    // Optional. Every data plane packet is guaranteed to be processed according
    // to table contents before the batch began, or after the batch completed
    // and the operations were programmed to the hardware. ...
    DATAPLANE_ATOMIC = 2;
  }
  Atomicity atomicity = 5;
}
```

上記より ```WriteRequest``` message では，コントロールプレーンを識別するための ```device_id``` ```role_id``` ```election_id``` に加えて ```Update``` message および ```atomicity``` を指定することが分かります． 
```atomicity``` は Entity の制御にエラーが発生した際の挙動を指定するパラメータで，それぞれ下記のように規定されているようです．

- CONTINUE_ON_ERROR
    - エラーが発生しても処理を続行する（特に復元処理は行わない）．
- ROLLBACK_ON_ERROR
    - エラー発生時は処理を無効化し，データプレーンを ```Write``` RPC 以前の状態に戻す．
- DATAPLANE_ATOMIC
    - 処理適用中のデータプレーンは ```Write``` RPC の直前の状態であることを保証する．

```Update``` message は p4runtime.proto で下記のように定義されています．

```protobuf

message Update {
  enum Type {
    UNSPECIFIED = 0;
    INSERT = 1;
    MODIFY = 2;
    DELETE = 3;
  }
  Type type = 1;
  Entity entity = 2;
}
```

```Write``` RPC で  Entity を制御する際は ```WriteRequest``` message に制御したい Entity の ```Update``` message を格納し（複数指定可），かつこれらの ```Update``` message に ```UpdateType``` と ```Entity``` の情報をそれぞれ格納する形になります．なお ```UpdateType``` としては下記の３パターンがあります．

- INSERT
    - 新規 Entity を登録する．
- MODIFY
    - 既存 Entity を修正する．
- DELETE
    - 既存 Entity を削除する．

このように Entity を制御する場合は ```WriteRequest``` message に上記で説明した各種情報を入力し ```Write``` RPC で server（データプレーン）側に通知することで，テーブルエントリ登録などの処理を行うことが出来ます．

### Entity の Read

Entity に関する情報の取得は ```Read``` RPC を用いて行います．コントロールプレーン側では ```ReadRequest``` message を ```Read``` RPC でデータプレーンに通知し，その返信として ```ReadResponse``` message を受け取ります．それぞれ p4runtime.proto で下記のように定義されています．

```protobuf

message ReadRequest {
  uint64 device_id = 1;
  repeated Entity entities = 2;
}

message ReadResponse {
  repeated Entity entities = 1;
}
```

情報を取得したい Entity を ```ReadRequest``` message に入れてデータプレーンに通知すると，その値が格納された Entity が返ってくる，という非常にシンプルな仕組みになっています．

```ReadRequest``` message では ```device_id``` のみを指定しています． この点について [specification](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html#sec-read-rpc) では下記のような記載があります．

> Since ReadRequests do not mutate any state on the switch, they do not require an election_id, and they do not require the presence of an open StreamChannel between the server and client.

```Read``` RPC はデータプレーンの状態を変更するものではないため，Entity の情報を読み込むだけならば Primary である必要はなく，Entity を含むデバイスがどれかが分かれば十分のようです． 

# おわりに

本記事では P4Runtime を用いてコントロールプレーンを実装する際の基礎として，下記について整理しました．

- P4Runtime の概要
- StreamChannel と Primary 選出
- ForwardingPipelineConfig の設定
- Entity の Write/Read

「[実装編](https://qiita.com/13ryuse4/items/3a68a79e81f621b85dd5)」では，これらの各処理を Go 言語を用いて実装し，[前回の記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)で実装した簡易 L2 Switch のテーブルエントリ登録とトラヒックカウンタ値取得を行います．

# 改訂記録

- P4Runtime の仕様更新 （2020.12.12)
    - 2020.12.1 付けで P4Runtime の仕様が v1.2.0 → v1.3.0 に更新されたことに伴い，下記修正しました．
        - P4Runtime 仕様のリンク更新
        - master の表記をそれぞれ primary に変更．