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


# はじめに

[P4Runtime](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html) を用いて下記を行うコントロールプレーンを Go 言語で実装します．

- テーブルエントリの登録
- マルチキャストグループの登録
- トラヒックカウンタ（Counter）値の取得

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)の続編となりますので続けて参照されることを推奨します． P4 ってそもそも何？という方は[こちらの記事](https://qiita.com/13ryuse4/items/cb83abd80712616e0799)を参照ください．なお，本記事で説明するコードは[こちら](https://github.com/ryu-s-4/p4-practice/tree/master/vlan-counter)で公開しました．動作確認のためのチュートリアルも用意していますので併せて参照ください．

# P4Runtime を用いた機能実装

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)でも触れたように，P4Runtime を用いた機能実装は大きく下記の流れとなります．

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

上記の各処理はインターフェース定義言語（protocol buffers）で抽象化して規定され [p4lang の github](https://github.com/p4lang/p4runtime/tree/master/proto/p4) で公開されていますが，特に Go 言語向けのライブラリは[こちら](https://github.com/p4lang/p4runtime/tree/master/go/p4)で公開されており，下記のように ```go get``` コマンドを実行することでこれらのライブラリを自作コントロールプレーンにて利用可能となります．

```shell:
> go get github.com/p4lang/p4runtime/go/p4/v1
> go get github.com/p4lang/p4runtime/go/p4/config/v1
```

Go 言語向けのライブラリは複数のファイルが存在しますが，メインとなるのは [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) で **P4Runtime を用いてデータプレーンを制御するための各種変数や関数**が定義されています．特に上述した各処理はいずれも下記で定義される ```P4RuntimeClient``` の具備する関数を用いて実装されます．

```go:p4runtime.pb.go
type P4RuntimeClient interface {
	Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
	Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (P4Runtime_ReadClient, error)
	SetForwardingPipelineConfig(ctx context.Context, in *SetForwardingPipelineConfigRequest, opts ...grpc.CallOption) (*SetForwardingPipelineConfigResponse, error)
	GetForwardingPipelineConfig(ctx context.Context, in *GetForwardingPipelineConfigRequest, opts ...grpc.CallOption) (*GetForwardingPipelineConfigResponse, error)
	StreamChannel(ctx context.Context, opts ...grpc.CallOption) (P4Runtime_StreamChannelClient, error)
	Capabilities(ctx context.Context, in *CapabilitiesRequest, opts ...grpc.CallOption) (*CapabilitiesResponse, error)
}
```

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)で P4Runtime による制御は gRPC を用いて実装されることを説明しました．そのため上記の ```P4runtimeClient``` も初期化時に gRPC connection を渡す必要があります．Go 言語では下記のように実装されます．

```go:init_p4runtimeclient.go

import(
    "google.golang.org/grpc"
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* gRPC connection 確立 */
    addr := "127.0.0.1" /* gRPC サーバのアドレス */
    port := "50051"     /* gRPC サーバのポート番号 */
    conn, err := grpc.Dial(addr+":"+port, grpc.WithInsecure())
    if err != nil {
        log.Fatal("ERROR: failed to establish gRPC connection. ", err)
    }
    defer conn.Close()

    /* P4runtime Client インスタンス生成 */
    client := v1.NewP4RuntimeClient(conn)
}
```

以降の処理は上記でインスタンス化した ```P4RuntimeClient``` の具備する各関数を用いて実装されます．以下では各処理について Go 言語で実装する上での要点を説明します．要点のみの説明となるため実装の細かい部分は[公開したコード](https://github.com/ryu-s-4/p4-practice/tree/master/vlan-counter)を参照ください．

## StreamChannel と Primary 選出

データプレーンと通信を行うためには，まずデータプレーンの制御 / データプレーンからの情報取得を行うための  ```StreamChannel``` をデータプレーンとの間で確立する必要があります．Go 言語では下記のように実装されます．

```go:init_streamchannel.go
import(
    "context"
)

func main(){

    /* StreamChannel 確立 */
    channel, err := client.StreamChannel(context.TODO())
    if err != nil {
        /* Error 処理 */
    }
}
```

```P4RuntimeClient``` の具備する ```StreamChannel()``` を用いてデータプレーンとの間で ```StreamChannel``` を確立しますが，この時点ではまだ Primary ではなく単に Channel が確立しただけなので，データプレーンの制御を行う場合は自身が Primary として選出される必要があります．こちらは Go 言語で下記のように実装されます．

```go:arbitration_update.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* MasterArbitrationUpdate */
    deviceid := /* device_id (uint64) */
    electionid := &v1.Uint128{
        High: /* election_id の上位64bit (uint64) */,
        Low:  /* election_id の下位64bit (uint64) */,
    }
    stream_request := v1.StreamMessageRequest{
        Update: &v1.StreamMessageRequest_Arbitration{
            Arbitration: &v1.MasterArbitrationUpdate{
                DeviceId:   deviceid,
                ElectionId: electionid,
            },
        },
    }

    err := channel.Send(stream_request)
    if err != nil {
        /* Error 処理 */
    }
    stream_response, err := channel.Recv()
    if err != nil {
        /* Error 処理 */
    }
}
```

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)で説明したように ```election_id``` が最大となるコントロールプレーンプロセスが Primary として選出されます．なお ```election_id``` は符号無し 128bit 整数のため，実装時は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) で定義される ```Uint128``` というデータ構造を用います（```High``` と ```Low``` という 2 つの符号無し 64 bit 整数で構成されています）．

StreamChannel の ```Send()``` により ```device_id``` と ```election_id``` を含む ```MasterArbitrationUpdate``` をデータプレーンに通知すると，データプレーン側で Primary 選出を行ってその結果をコントロールプレーンに返信します．コントロールプレーンはデータプレーンからの返信を ```Recv()``` で待機します．

```Send()``` の引数のデータ構造はやや複雑ですが，```StreamMessageRequest``` という変数の中に ```Update``` を格納する形をしており，上記で使用した ```MasterArbitrationUpdate``` は Primary 選出を行うための ```Update``` の一種となります．```Update```の候補としては，上記の ```MasterArbitrationUpdate``` 以外に ```PacketOut``` や ```DigestListAck``` がありますが，詳細は割愛します．[Digest](https://p4.org/p4-spec/docs/PSA-v1.1.0.html#sec-packet-digest) はデータプレーンからコントロールプレーンに情報を通知する機能のため，データプレーンとコントロールプレーン間でのパケットやパケットの中身に関する情報交換に使用するようです．詳細は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) 等を参照下さい．

上記では省略していますが ```MasterArbitrationUpdate``` には ```role_id``` を指定することが可能で，そのコントロールプレーンが Primary に選出された場合は ```role_id``` で規定される範囲内でデータプレーンの排他的な制御が可能となります（何も指定しない場合は default role となり full pipeline access が可能となります）．

## ForwardingPipelineConfig の設定

データプレーンとの間で ```StreamChannel``` を確立し Primary に選出されると，次は ```ForwardingPipelineConfig``` をデータプレーンに設定する必要があります．```ForwardingPipelineConfig``` は端的に言うとデータプレーンの設定ファイルで，P4Runtime では下記のようなデータ構造で扱われます．

```go:p4runtime.pb.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/config/v1"
)

type ForwardingPipelineConfig struct {
	P4Info            *v1.P4Info
	P4DeviceConfig    []byte           
	Cookie            *ForwardingPipelineConfig_Cookie 
}
```
上記で重要な変数は ```P4Info``` と ```P4DeviceConfig``` で，それぞれ下記を格納します．

- ```P4Info```：データプレーンの各要素（Table や Action 等）を制御するための識別子
- ```P4DeviceConfig```：データプレーンの挙動を規定する設定

いずれも P4 プログラムのコンパイル時に生成されるファイルをロードして各変数に値を格納します．例えば下記のように自作データプレーンである ```switching.p4``` をコンパイルすると ```P4Info``` のためのファイルとして ```p4info.txt```が，```P4DeviceConfig```のためのファイルとして ```switching.json``` がそれぞれ生成されます．

```shell
> p4c --std p4_16 -b bmv2 --p4runtime-files p4info.txt switching.p4
```

なお，上記の P4 コンパイラは [BMv2](https://github.com/p4lang/behavioral-model)（P4動作確認用ソフトウェア）のために[こちら](https://github.com/p4lang/p4c)で公開されているもので，実際には使用するデバイス毎に P4コンパイラが異なります．

上記ファイルより ```ForwardingPipelineConfig``` の各変数に値を格納する部分は下記のように実装されます．

```go:init_config.go
import(
    "ioutil"
    "github.com/golang/protobuf/proto"
    config_v1 "github.com/p4lang/p4runtime/go/p4/config/v1"
)

func main(){

    /* InitConfig (P4Info / ForwardingPipelineConfig) */
    p4infoPath  := /* p4info.txt のファイルパス */
    devconfPath := /* switching.json のファイルパス */

    p4info := &config_v1.P4Info{}
    p4infoBytes, err := ioutil.ReadFile(p4infoPath)
    if err != nil {
        /* Error 処理 */
    }
    err = proto.UnmarshalText(string(p4infoBytes), p4info)
    if err != nil {
        /* Error 処理 */
    }

    devconf, err := ioutil.ReadFile(devconfPath)
    if err != nil {
        /* Error 処理 */
    }
    config := &v1.ForwardingPipelineConfig{
        P4Info:         p4info,
        P4DeviceConfig: devconf,
    }
}
```

```ForwardingPipelineConfig``` は ```P4RuntimeClient``` の ```SetForwardingPipelineConfig()``` を用いてデータプレーンに設定します．こちらは下記のように実装されます．

```go:set_config.go
import(
    "context"

    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* SetForwardingPipelineConfig */
    actionType := /* Action Type を指定 */
    var action v1.SetForwardingPipelineConfigRequest_Action
    switch actionType {
    case "VERIFY":
        action = v1.SetForwardingPipelineConfigRequest_VERIFY
    case "VERIFY_AND_SAVE":
        action = v1.SetForwardingPipelineConfigRequest_VERIFY_AND_SAVE
    case "VERIFY_AND_COMMIT":
        action = v1.SetForwardingPipelineConfigRequest_VERIFY_AND_COMMIT
    case "COMMIT":
        action = v1.SetForwardingPipelineConfigRequest_COMMIT
    case "RECONCILE_AND_COMMIT":
        action = v1.SetForwardingPipelineConfigRequest_RECONCILE_AND_COMMIT
    default:
        action = v1.SetForwardingPipelineConfigRequest_UNSPECIFIED
    }
    set_request := v1.SetForwardingPipelineConfigRequest{
        DeviceId:   deviceid,
        ElectionId: electionid,
        Action:     action,
        Config:     config,
    }
    set_response, err := client.SetForwardingPipelineConfig(context.TODO(), &set_request)
    if err != nil {
        /* Error 処理 */
    }
}
```

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1)でも触れたように ```SetForwardingPipelineConfig()``` ではコントロールプレーンプロセスの識別のための ```device_id``` と ```election_id``` およびデータプレーンの設定である ```ForwardingPipelineConfig``` に加えて，下記いずれかの ```Action```を設定します．

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

上記が問題なく完了すると自作データプレーンがデバイスにロードされ，テーブルエントリの登録やトラヒックカウンタ値の取得等が可能となります．

## Entity の Write/Read

テーブルエントリの登録やトラヒックカウンタ値の取得は ```Entity```（P4Runtime で制御可能な要素）の ```Write``` や ```Read``` という形で実装されます． [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) では ```Entity``` を下記のように定義しています．

```go:p4runtime.pb.go
type Entity struct {
	// Types that are valid to be assigned to Entity:
	//	*Entity_ExternEntry
	//	*Entity_TableEntry
	//	*Entity_ActionProfileMember
	//	*Entity_ActionProfileGroup
	//	*Entity_MeterEntry
	//	*Entity_DirectMeterEntry
	//	*Entity_CounterEntry
	//	*Entity_DirectCounterEntry
	//	*Entity_PacketReplicationEngineEntry
	//	*Entity_ValueSetEntry
	//	*Entity_RegisterEntry
	//	*Entity_DigestEntry
	Entity               isEntity_Entity
}
```

上記のように ```Entity``` としては様々な種類のものが定義されています．各要素の詳細は[公式ページ](https://p4.org/p4runtime/spec/v1.3.0/P4Runtime-Spec.html#sec-p4info-objects)等を参照ください．今回は次の３つの処理を行いますが，関連する ```Entity``` はそれぞれ下記となります．

- テーブルエントリの登録： ```Entity_TableEntry```
- マルチキャストグループの登録： ```Entity_PacketReplicationEngineEntry```
- トラヒックカウンタ値の取得： ```Entity_CounterEntry```

テーブルエントリ / マルチキャストグループの登録を行う場合は，上記の ```Entity``` を ```Update``` に格納して ```Write()``` でデータプレーンに通知します． [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) では ```Update``` を下記のように定義しています．

```go:p4runtime.pb.go
type Update_Type int32

const (
	Update_UNSPECIFIED Update_Type = 0
	Update_INSERT      Update_Type = 1
	Update_MODIFY      Update_Type = 2
	Update_DELETE      Update_Type = 3
)

type Update struct {
    Type    Update_Type 
    Entity  *Entity
}
```

[前回の記事](https://qiita.com/13ryuse4/items/96ed8b31382e1fdd79f1) でも触れたように ```Type```には ```Update``` の種類として次のいずれかを格納します．

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

```Update``` を新たに生成する部分は Go 言語で下記のように実装されます．

```go:prepare_update.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Prepare Update */
    updateType := /* Update Type を指定 */
    var type v1.Update_Type
    switch updateType {
    case "INSERT":
        type = v1.Update_INSERT
    case "MODIFY":
        type = v1.Update_MODIFY
    case "DELETE":
        type = v1.Update_DELETE
    default:
        type = v1.Update_UNSPECIFIED
    }
    update := &v1.Update{
        Type: type,
        Entity: /* 任意の Entity */
    }
}

```

上記の ```Update``` をデータプレーンに通知する場合は ```WriteRequest``` に上記 ```Update``` 等を格納して ```Write()``` に渡します．これらは Go 言語で下記のように実装されます．

```go:write_entity.go
import(
    "context"

    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Write Entities */
    atomicityType := /* Atomicity を指定 */
    var atomicity v1.WriteRequest_Atomicity
    switch atomicityType {
    case "CONTINUE_ON_ERROR":
        atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
    case "ROLLBACK_ON_ERROR": // OPTIONAL
        atomicity = v1.WriteRequest_ROLLBACK_ON_ERROR
    case "DATAPLANE_ATOMIC": // OPTIONAL
        atomicity = v1.WriteRequest_DATAPLANE_ATOMIC
    default:
        atomicity = v1.WriteRequest_CONTINUE_ON_ERROR
    }
    write_request := v1.WriteRequest{
        DeviceId:   deviceid,
        ElectionId: electionid,
        Updates:    []*v1.Update{update},
        Atomicity:  atomicity,
    }
    write_response, err := client.Write(context.TODO(), &write_request)
    if err != nil {
        /* Error 処理 */
    }
}
```

```Write()``` の引数である ```WriteRequest``` にはコントロールプレーンプロセスの識別のための ```device_id``` / ```election_id``` および ```Update``` に加えて ```Entity``` の更新方法として下記いずれかの ```Atomicity``` を格納します．

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

なお ```WriteRequest``` 内の ```Update``` は複数指定が基本である点に注意してください． Go で実装する場合は ```Update``` が１つの場合もスライスで実装する形となります．

```Update``` 内の ```Type``` および ```Entity``` に所望の情報を入力して上記を実施することでテーブルエントリやマルチキャストグループの登録が可能です．なお，各 ```Entity``` の中身については ```Entity``` の種類毎にやや複雑なデータ構造となっているため，それぞれ取り上げて後述します．

ここまでは ```Entity``` の制御に関する説明ですが，トラヒックカウンタ値の取得等，データプレーンからの情報取得も ```Entity``` および ```Read()``` を用いて実装出来ます．こちらは下記のように実装されます．

```go:read_entity.go
import(
    "context"

    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Create Read Client */
    read_request := v1.ReadRequest{
        DeviceId: deviceid,
        Entities: /* 任意の Entity． []*v1.Entity である点に注意 */,
    }
    read_client, err := client.Read(context.TODO(), &read_request)
    if err != nil {
        /* Error 処理 */
    }
```

```Read()``` の場合は非常にシンプルで，情報を取得したい対象デバイスの ```device_id``` および取得したい情報を表す ```Entity``` を格納した ```ReadRequest``` を ```Read()``` でデータプレーンに通知すると，その ```Entity``` の情報を取得するための ```ReadClient``` が返ってきます． なお，上記で指定する ```Entity``` は複数（Go 言語でいうスライスで）格納する点に注意してください．１つの ```Entity``` を取得したい場合でも要素数が１つのスライスを用いる必要があります．

実際にデータプレーンからデータを取得する際は所望のタイミングで ```ReadClient``` の ```Recv()``` を用いることでデータ取得が可能です．こちらは Go 言語で下記のように実装されます．

```go:retrieve_data.go

func main(){

    /* Read Data from DP */
    read_response, err := read_client.Recv()
    if err != nil {
        /* Error 処理 */
    }
    entities := read_response.GetEntities()
    if entity == nil {
        /* Error 処理 */
    }
}
```

```Recv()``` から ```ReadResponse``` （上記の ```response```）を受け取り，その中に所望のデータを含む ```Entity``` が指定した分だけ格納されています．```Entity``` を取り出す際は ```GetEntities()``` を使用します（```response``` から直接取り出すことも可能です）．なお，取り出した ```Entity``` もスライスのため各情報を取得する際は実装に注意が必要です． 詳細なデータ構造等は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) を参照ください．

以上が P4Runtime で ```Entity``` を制御 or ```Entity``` から情報を取得する実装になります．以降は，今回使用する各 ```Entity```（```Entity_TableEntry``` ```Entity_PacketReplicationEngineEntry``` ```Entity_CounterEntry```）について詳細なデータ構造を示しつつ ```Entity``` を生成する部分の実装について説明します．

### テーブルエントリのデータ構造

テーブルエントリを表す ```Entity_TableEntry``` および関連する各変数は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) でそれぞれ下記のように定義されています．

```go:p4runtime.pb.go
type Entity_TableEntry struct {
    TableEntry    *TableEntry 
}

// Entity_TableEntry の "TableEntry" に格納する変数 
type TableEntry struct {
    TableId            uint32
    Match              []*FieldMatch
    Action             *TableAction 
    Priority           int32
    MeterConfig        *MeterConfig 
    CounterData        *CounterData
    IsDefaultAction    bool
    IdleTimeoutNs      int64 
    TimeSinceLastHit   *TableEntry_IdleTimeout 
    Metadata           []byte
}

// TableEntry の "Match" に格納する変数
type FieldMatch struct {
    FieldId uint32
    // Types that are valid to be assigned to FieldMatchType:
    //	*FieldMatch_Exact_
    //	*FieldMatch_Ternary_
    //	*FieldMatch_Lpm
    //	*FieldMatch_Range_
    //	*FieldMatch_Optional_
    //	*FieldMatch_Other
    FieldMatchType    isFieldMatch_FieldMatchType 
}

// FieldMatch の "FieldMatchType" に格納する変数（他にも候補あり）
type FieldMatch_Exact_ struct {
    Exact    *FieldMatch_Exact
}

// FieldMatch_Exact_ の "Exact" に格納する変数
type FieldMatch_Exact struct {
    Value    []byte
}

// TableEntry の "Action" に格納する変数
type TableAction struct {
    // Types that are valid to be assigned to Type:
    //	*TableAction_Action
    //	*TableAction_ActionProfileMemberId
    //	*TableAction_ActionProfileGroupId
    //	*TableAction_ActionProfileActionSet
    Type    isTableAction_Type
}

// TableAction の "Type" に格納する変数（他にも候補あり）
type TableAction_Action struct {
    Action    *Action
}

// TableAction_Action の "Action" に格納する変数
type Action struct {
    ActionId    uint32
    Params      []*Action_Param
}

// Action の "Params" に格納する変数
type Action_Param struct {
    ParamId    uint32
    Value      []byte
}
```

上記は一部のみ記載していますが```Entity_TableEntry``` ひとつとっても複数のパラメータによる入子構造になっており，非常に複雑なデータ構造となっています．利用する ```Entity``` 毎に [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) を参照しつつデータ構造を把握する必要があります．

今回使用する [P4 プログラム](https://github.com/ryu-s-4/p4-practice/blob/master/vlan-counter/switching.p4) で定義する L2 転送用のテーブル（```mac_exact```）を例に ```Entity_TableEntry``` がどのように生成されるかを説明します．使用するテーブルは下記のような感じです（エントリはあくまで一例です）．

![スクリーンショット 2020-12-20 19.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651428/2d84ca65-913b-9248-51f5-ce47e3b3f316.png)

赤字は ```Entity_TableEntry``` 生成時に指定すべき項目，青字は今回使用する [P4 プログラム](https://github.com/ryu-s-4/p4-practice/blob/master/vlan-counter/switching.p4)での識別名，緑字はコントロールプレーンから設定する値をそれぞれ表しています．例えば，上記の No.1 のエントリを表す ```Entity_TableEntry``` は下記のように生成されます．

```go:entity_tableentry.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Entity_TableEntry の生成 */
    entity_tableentry := &v1.Entity_TableEntry{
        TableEntry: &v1.TableEntry{
            TableId: /* "mac_exact" の table-id を p4info.txt から取得 */,
            Match:   []*v1.FieldMatch{
                &v1.FieldMatch{
                    FieldId: /* "hdr.ethernet.dstAddr" の field-id を p4info.txt から取得 */,
                    FieldMatchType: &v1.FieldMatch_Exact_{
                        Exact: &v1.FieldMatch_Exact{
                            Value: /* "aa:bb:cc:dd:ee:ff" in []byte */,
                        },
                    },
                }
            },
            Action: &v1.TableAction{
            Type: &v1.TableAction_Action{
                Action: &v1.Action{
                    ActionId: /* "switching" の action-id を p4info.txt から取得 */,
                    Params:   []*v1.Action_Param{
                        &v1.Action_Param{
                            ParamId: /* "port" の param-id を p4info.txt から取得 */,
                            Value:   /* 0 in []byte */,
                        }
                    },
                },
            },
        },
    }
}
```

上記の通り ```Table``` や ```Action``` 等を指定するために ```p4info.txt``` を参照しつつ各要素の識別名からデータプレーンでの識別子を取得する必要があります．また ```match``` の key 値や ```action param.``` の値は []byte 形式で格納し，かつバイト数はその値を格納するために必要最低限のバイト数とする必要があります（例えば上記テーブルの ```port``` は 9bit のため 2byte のデータとする必要があります）．

[今回の実装](https://github.com/ryu-s-4/p4-practice/blob/master/vlan-counter/myutils/helper.go)では上記の "指定すべき項目（赤字記載のパラメータ）" を格納する Helper 変数（ ```TableEntryHelper``` という変数）を用意し，こちらの変数から ```Entity_TableEntry``` を生成する関数（ ```BuildTableEntry()``` という関数）を使用して ```Entity``` を生成しています．なお，コントロールプレーンから設定すべき値（緑字記載のパラメータ）は ```TableEntryHelper``` のための json ファイル（[runtime.json](https://github.com/ryu-s-4/p4-practice/blob/master/vlan-counter/runtime.json)）に記載し，こちらを読み込む形で実装しています．

### マルチキャストグループのデータ構造

マルチキャストグループを表す ```Entity_PacketReplicationEngineEntry``` および関連する各変数は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) でそれぞれ下記のように定義されています．

```go:p4runtime.pb.go
type Entity_PacketReplicationEngineEntry struct {
    PacketReplicationEngineEntry *PacketReplicationEngineEntry
}

/* Entity_PacketReplicationEngineEntry の "PacketReplicationEngineEntry" に格納する変数 */
type PacketReplicationEngineEntry struct {
	// Types that are valid to be assigned to Type:
	//	*PacketReplicationEngineEntry_MulticastGroupEntry
	//	*PacketReplicationEngineEntry_CloneSessionEntry
	Type                 isPacketReplicationEngineEntry_Type
}

/* PacketReplicationEngineEntry の "Type" に格納する変数 */
type PacketReplicationEngineEntry_MulticastGroupEntry struct {
	MulticastGroupEntry *MulticastGroupEntry
}

/* PacketReplicationEngineEntry の "MulticastGroupEntry" に格納する変数 */
type MulticastGroupEntry struct {
	MulticastGroupId     uint32
	Replicas             []*Replica
}

/* MulticastGroupEntry の "Replica" に格納する変数 */
type Replica struct {
	EgressPort           uint32
	Instance             uint32
}
```
例えば出力先ポートが ```0``` ```1``` ```2``` となるマルチキャストグループを表す ```Entity_PacketRepolicationEngineEntry``` は下記のように生成されます．

```go:entity_packetreplicationengineentry.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Entity_PacketReplicationEngineEntry の生成 */
    entity_packetreplicationengineentry := &v1.Entity_PacketReplicationEngineEntry{
        PacketReplicationEngineEntry: &v1.PacketReplicationEngineEntry{
            Type: &v1.PacketReplicationEngineEntry_MulticastGroupEntry{
                MulticastGroupEntry: &v1.MulticastGroupEntry{
                    MulticastGroupId: /* 任意のマルチキャストグループエントリ ID． コントロールプレーンで管理． */,
                    Replicas:         []*v1.Replica{
                        &v1.Replica{
                            EgressPort: /* 0 in uint32 */,
                            Instance:   /* EgressPort の組を管理するための識別子 */,
                        },
                        &v1.Replica{
                            EgressPort: /* 1 in uint32 */,
                            Instance:   /* EgressPort の組を管理するための識別子 */,
                        },
                        &v1.Replica{
                            EgressPort: /* 2 in uint32 */,
                            Instance:   /* EgressPort の組を管理するための識別子 */,
                        }
                    },
                },
            },
        },
    }
}
```

```MulticastGroupId``` は ```EgressPort``` の組み合わせ毎にコントロールプレーン側で管理する識別子となります．

### トラヒックカウンタ値のデータ構造

トラヒックカウンタ値の取得には ```Entity_CounterEntry``` を使用します．```Entity_CounterEntry``` および関連する各変数は [p4runtime.pb.go](https://github.com/p4lang/p4runtime/blob/master/go/p4/v1/p4runtime.pb.go) でそれぞれ下記のように定義されています．

```go:p4runtime.pb.go
type Entity_CounterEntry struct {
	CounterEntry *CounterEntry
}

/* Entity_CounterEntry の "CounterEntry" に格納する変数 */
type CounterEntry struct {
	CounterId            uint32       
	Index                *Index
	Data                 *CounterData
}

/* CounterEntry の "Index" に格納する変数 */
type Index struct {
	Index                int64
}

/* CounterEntry の "Data" に格納する変数（Read の場合はデータプレーン側で値を格納する） */
type CounterData struct {
	ByteCount            int64
	PacketCount          int64
}
```

今回使用する [P4 プログラム](https://github.com/ryu-s-4/p4-practice/blob/master/vlan-counter/switching.p4) では ```traffic_cnt``` という識別名で Counter を定義しているため，例えば ```index```が 100 のトラヒックカウンタ値を取得したい場合，下記のように ```Entity_CounterEntry``` を生成し ```Read()``` を使用することでデータプレーンから所望のデータを取得出来ます．なお，データプレーンからは上記の ```CounterData``` に値が格納された形で ```Entity``` が返ってきます．

```go:entity_counterentry.go
import(
    v1 "github.com/p4lang/p4runtime/go/p4/v1"
)

func main(){

    /* Entity_CounterEntry の生成 */
    entity_counterentry := &v1.Entity_CounterEntry{
        CounterEntry: &v1.CounterEntry{
            CounterId: /* "traffic_cnt" の counter-id を p4info.txt から取得 */,
            Index: &v1.Index{
                Index: /* 100 in int64 */,
            },
        },
    }
}
```

テーブルエントリの場合と同様に ```CounterId``` は P4 プログラムで定義したカウンタ名から ```p4info.txt``` を参照しつつ取得する必要があります．

# おわりに

本記事では P4Runtime を用いて下記を行う Go 実装について説明しました．

- テーブルエントリの登録
- マルチキャストグループの登録
- トラヒックカウンタ（Counter）値の取得

今回は一部の ```Entity```（P4Runtime で制御可能な要素）のみを説明しましたが，その他の ```Entity``` についても基本は同様で，必要な ```Entity``` 変数を用意し ```Write()``` あるいは ```Read()``` を用いることでデータプレーンの制御 or データプレーンからの情報取得が可能となります．