---
title: 控制面开发详细步骤
description: "1. 开发者可以在自定义代码工程下创建OVS控制面开发目录。"
url: https://www.hikunpeng.com/document/detail/zh/flexda/devguide/dpak_dev0070.html
sourcePath: /source/zh/flexda/devguide/dpak_dev0070.html
indexId: 874a8471995cb86f0180ddf4302e370ac17ca7cee9ca25ac1df443415c164cc844
---
# 控制面开发详细步骤

#### 开发前准备

1. 开发者可以在自定义代码工程下创建OVS控制面开发目录。
  示例如下，详情可参见自定义代码工程目录
  。

```
ovs_project/
├── CMakeLists.txt                   # 顶层CMake配置文件
├── src_control_plane                # 用户OVS控制面源代码目录
├── CMakeLists.txt               # OVS控制面CMake配置文件
└── ...                          # OVS控制面源代码
└── src_dsl                          # 用户数据面源代码子目录
├── CMakeLists.txt               # 数据面的子目录CMake配置文件
└── ovs_project.hdr              # 数据面Hydra源文件
```


2. 复制固件编译生成的hydra_info.h头文件到 src_control_plane下。
  详情可参见执行固件编译
  。

```
cd {$project_path}/ovs_project
cp src_dsl/build/hydra_info.h src_control_plane/
```


3. 在代码中引入编程框架提供的OVS控制面头文件，包括hook.h和libapi.h。
  复制“flexda_sdk/include/control_plane/”
  中的hook.h和libapi.h到src_control_plane下。

```
cd {$sdk_path}/flexda_sdk/
cp include/control_plane/* {$project_path}/ovs_project/src_control_plane/
```


#### 步骤一：开发OVS控制面代码

进行OVS控制面源代码的开发，通过代码和libapi.h中提供的接口，实现hook.h中定义的hook函数。

全局初始化和反初始化hook函数为可选实现，其他hook函数为必选实现。源代码中如未提供实现其他hook函数，编译安装产物后会导致OVS启动不成功。


1. 开发代码前根据业务场景以及Hydra数据面代码，识别需要下发硬件流表的报文类型以及对应的识别特征。接着从Hydra数据面代码和hydra_info.h中提取每种报文类型应该下发流表项到哪张表（table）、在表中匹配哪些字段（key）、需要对流表行为（action）做哪些修改。
以处理入向VLAN流量，执行VLAN POP和内层RSS action的场景为例。

  - Hydra数据面代码

```
#include "hydra_model.hdr"
#include "hydra_parser.hdr"
#include "hydra_externs.hdr"
#include "hydra_builtin.hdr"
#define VPORT_TYPE_BOND 3
control MyMainPipe(in hdr_fix_headers hdr) {
rte_vlan_hdr_t vlan_key;
bit<16> inner_eth;
@main
table vlan_exact {
key = {
vlan_key         : exact;
inner_eth        : exact;
}
actions = {
rte_of_pop_vlan;
rte_port_id;
flexda_inner_rss;
}
size = 0x100000;
default_action = flexda_no_action;
aging_time = 0;
}
apply {
if ((hdr_input_port_id_get() >> 12) == VPORT_TYPE_BOND) { // 判断报文为RX
if (hdr.vlan.isValid()) { // 判断报文为VLAN RX
if (hdr.ethernet.isValid()) {
inner_eth = hdr.ethernet.eth_type; // 提取VLAN类型
}
vlan_key.vlan_tci = (bit<16>)((((bit<16>)hdr.vlan.pri) << 13) | ((bit<16>)hdr.vlan.vid)); // 提取VLAN id
if (vlan_exact.apply().miss) { // 匹配流表
hdr_ovs_pipeline_action_upcall();
}
}
}
}
}
control MyPostPipe(in hdr_fix_headers hdr) {
apply {}
}
Switch(
TX_OVS(hdr_fix_parser(),MyMainPipe(),MyPostPipe()),
RX_OVS(hdr_fix_parser(),MyMainPipe(),MyPostPipe())
) main;
```


  - Hydra数据面生成的hydra_info.h

```
... //一些hydra结构体定义
enum hydra_group_type { // table id
HYDRA_TABLE_INVALID = 0x0000,
HYDRA_TABLE_MYMAINPIPE_VLAN_EXACT,
};
enum hydra_default_action_type { // hydra默认action类型枚举
DEFAULT_FLOW_ACTION_TYPE_UPCALL = 359,
DEFAULT_FLOW_ACTION_TYPE_CT = 357,
DEFAULT_FLOW_ACTION_TYPE_INNER_RSS = 364,
};
enum hydra_action_type { // hydra自定义action类型枚举
HYDRA_ACTION_INVALID = 0x1000,
};
enum hydra_item_type { // hydra自定义key类型枚举
HYDRA_ITEM_INVALID = 0x2000,
HYDRA_ITEM_MYMAINPIPE_VLAN_EXACT_INNER_ETH,
};
int table_patterns_mymainpipe_vlan_exact [2] = { //table包含的key类型
RTE_FLOW_ITEM_TYPE_VLAN,
HYDRA_ITEM_MYMAINPIPE_VLAN_EXACT_INNER_ETH,
};
int table_actions_mymainpipe_vlan_exact [4] = { //table包含的action类型
RTE_FLOW_ACTION_TYPE_COUNT,
RTE_FLOW_ACTION_TYPE_OF_POP_VLAN,
RTE_FLOW_ACTION_TYPE_PORT_ID,
DEFAULT_FLOW_ACTION_TYPE_INNER_RSS,
};
... //
```


Hydra数据面对VLAN RX流量处理时从报文头中提取VLAN类型(inner_eth)以及VLAN ID作为key，在vlan_exact精确表中匹配，可执行action包括rte_of_pop_vlan、rte_port_id和flexda_inner_rss。
hydra_info.h中定义了以下枚举值：

  - vlan_exact表枚举值：HYDRA_TABLE_MYMAINPIPE_VLAN_EXACT
  - inner_eth key枚举值：HYDRA_ITEM_MYMAINPIPE_INNER_ETH_TYPE
  - vlan_key key枚举值：RTE_FLOW_ITEM_TYPE_VLAN
VLAN RX流量会先命中OVS带有RTE_FLOW_ACTION_TYPE_OF_POP_VLAN & RTE_FLOW_ACTION_TYPE_PORT_ID action的软件流表，组装硬件流表时需添加软件流表不包含的DEFAULT_FLOW_ACTION_TYPE_INNER_RSS action。
可将以上分析归纳为下表：
**表1 控制面代码开发前分析示例**

| 报文类型 | 识别特征 | table | key | action |
| --- | --- | --- | --- | --- |
| VLAN RX | OVS流表action包含VLAN POP | HYDRA\_TABLE\_MYMAINPIPE\_VLAN\_EXACT | RTE\_FLOW\_ITEM\_TYPE\_VLAN | 添加DEFAULT\_FLOW\_ACTION\_TYPE\_INNER\_RSS |
| VLAN RX | OVS流表action包含VLAN POP | HYDRA\_TABLE\_MYMAINPIPE\_VLAN\_EXACT | HYDRA\_ITEM\_MYMAINPIPE\_INNER\_ETH\_TYPE | 添加DEFAULT\_FLOW\_ACTION\_TYPE\_INNER\_RSS |


2. 根据需要，定义OVS运行时全局变量。并在初始化/反初始化hook函数的实现中处理全局变量的初始化/反初始化。

```
flexda_custom_api_t g_api = {0};
/*... 其他全局变量 ...*/
int flexda_custom_init(flexda_custom_api_t api)
{
g_api = api;
/*... 其他全局变量初始化 ...*/
return 0;
}
int flexda_custom_deinit(void)
{
/*... 其他全局变量反初始化 ...*/
return 0;
}
```


3. 根据业务场景需要，定义OVS控制面自定义报文上下文（同报文在不同流表卸载流程中拥有同一份报文上下文），并实现自定义报文上下文初始化/反初始化hook函数。
  自定义报文上下文中最少应保存：报文类型标记、各类型报文的原始报文头信息。

```
// 参见以下示例代码，展示处理单层VLAN RX场景时如何自定义报文上下文。
struct custom_packet_ctx {
// 报文场景标志位
uint8_t vlan_poped;  // VLAN RX报文的标志位
/* ... 其他标志位 ...*/
// 报文头信息
struct flexda_sub_hdr out_header;  // 该报文头信息保存外层的l4\l3\l2报文头信息
/* ... 其他报文头信息 ...*/
// 其他业务需要的报文上下文
/* ... 其他上下文 ... */
};
int flexda_custom_init_ctx(void **ctx_addr)
{
// 为自定义报文上下文申请内存
struct custom_packet_ctx *ctx = (struct custom_packet_ctx *)flexda_calloc(1, sizeof(struct custom_packet_ctx));
if (ctx == NULL) {
return -EINVAL;
}
*ctx_addr = (void *)ctx;
ctx->vlan_poped = 0;
/* ... 其他上下文初始化赋值 ...*/
return 0;
}
int flexda_custom_deinit_ctx(void **ctx_addr)
{
if (ctx_addr == NULL || *ctx_addr == NULL) {
return -EINVAL;
}
struct custom_packet_ctx *ctx = (struct custom_packet_ctx *)*ctx_addr;
// 为自定义报文上下文释放内存
flexda_free(ctx);
*ctx_addr = NULL;
return 0;
}
```


4. 根据业务场景，在更新上下文和提取报文头的hook函数实现中，用代码指示更新上下文和提取报文头的逻辑。

  - flexda_custom_update_ctx最少应实现：读取入参info判断报文类型，并将报文类型写入ctx。
  - flexda_custom_extract_hdr最少应实现：读取入参ctx判断报文类型，并将该类型报文的原始报文头信息解析并写入在ctx。

```
// 参见以下示例代码，当前卸载的OVS流表action中包含VLAN_POP时判定报文场景为VLAN RX，vlan_poped置为1
int flexda_custom_update_ctx(void *ctx, flexda_custom_info_t info)
{
struct custom_packet_ctx *custom_ctx = (struct custom_packet_ctx *)ctx;
struct rte_flow_action *actions = info.src_actions;
for (int i = 0; i < 15; i++) {
switch (actions[i]->type) {
case RTE_FLOW_ACTION_TYPE_OF_POP_VLAN:
custom_ctx->vlan_poped = 1;
break;
/*** 其他场景更新上下文标志位逻辑 ***/
default:
break;
}
}
/*** 其他场景更新上下文逻辑 ***/
return 0;
}
// 参见以下示例代码，提取报文头时若vlan_poped为1则提取VLAN RX报文组key需要的报文头信息。
int flexda_custom_extract_hdr(void *ctx, flexda_custom_info_t info)
{
struct custom_packet_ctx *custom_ctx = (struct custom_packet_ctx *)ctx;
struct flexda_sub_hdr *out_header = &custom_ctx->out_header;  // 框架提供的默认报文头信息结构体，搭配报文解析api使用
struct dp_packet *pkt = info.pkt;
struct flexda_extract_ctx pkt_parser;  // 框架提供的默认报文解析结构体，搭配报文解析api使用
pkt_parser.pkt_itr = (uint8_t *)flexda_dp_packet_data(pkt);
pkt_parser.pkt_end = (uint8_t *)flexda_dp_packet_data(pkt) + flexda_dp_packet_size(pkt);
if (custom_ctx->vlan_poped == 1) {
// 调用框架提供的默认报文头解析API，将报文头信息解析保存在报文自定义上下文中
if (!flexda_extract_hdr(&pkt_parser, out_header)) { // 解析不成功
return -EINVAL;
}
return 0; // 解析成功
}
/*** 其他场景提取报文头信息逻辑 ***/
return false;// 标志位不为1
}
```


5. 根据业务场景和Hydra代码实现的数据面逻辑，实现对应匹配的key/action/attribute构造hook函数。

  - flexda_custom_construct_attr最少应实现：读取入参ctx判断报文类型以及对应的原始报文头信息，将报文类型对应的表id（参见    1
识别结果）写入attr->group。
  - flexda_custom_construct_key最少应实现：读取入参ctx判断报文类型以及对应的原始报文头信息，调用接口将报文类型所有匹配字段（参见    1
识别结果）写入参数hw_key。
  - flexda_custom_construct_action最少应实现：读取入参ctx判断报文类型以及对应的原始报文头信息，调用接口将对参数hw_action中的流表行为做添加删除或转换（参见    1
识别结果）。
- flexda_custom_construct_attr中指定table时，使用hydra_info.h中的枚举值enum hydra_group_type，见  Group、Action、Item枚举
。
- flexda_custom_construct_key中插入key时，在hydra_info.h中找到对应table的key，见  表Key信息
；根据Hydra代码定义的匹配逻辑，调用对应接口插入需要的key，key类型使用  表Key信息
中的枚举值。
- flexda_custom_construct_action中插入action时，在hydra_info.h中找到对应table支持的action列表，见  表Action讯息
；按业务需要插入或转换流表hydra action，action类型使用  表Action讯息
中的枚举值，action data参见  HydraAction讯息
中对应的结构体进行组装。


```
//注：此处调用时attr的group值默认为1
// 参见以下示例代码：当处理VLAN RX报文时（vlan_poped为1），下发到Hydra数据面定义的VLAN精确流表（HYDRA_TABLE_MYMAINPIPE_VLAN_EXACT）。
int flexda_custom_construct_attr(void *ctx, flexda_custom_info_t info, struct rte_flow_attr *attr)
{
struct custom_packet_ctx *custom_ctx = (struct custom_packet_ctx *)ctx;
// 根据自定义上下文，将Hydra table的枚举值赋给attr中的group来指定下发的table
if (custom_ctx->vlan_poped == 1) {
attr->group = HYDRA_TABLE_MYMAINPIPE_VLAN_EXACT;
}
/*** 其他场景指定下发table ***/
return 0;
}
// 注：此处调用时hw_key为空，需要用户将Hydra数据面需要的key全部实现插入
// 参见以下示例代码：当处理VLAN RX报文时（vlan_poped为1），从外层报文头out_header提取vlan和encap，调用接口插入hw_key中。（例如Hydra数据面代码处理VLAN RX报文时仅做vlan和encap的匹配）
int flexda_custom_construct_key(void *ctx, flexda_custom_info_t info, struct rte_flow_item *hw_key)
{
struct custom_packet_ctx *custom_ctx = (struct custom_packet_ctx *)ctx;
struct flexda_sub_hdr *out_header = &custom_ctx->out_header;
// 根据上下文条件控制组key流程，需要与Hydra里为table定义的key匹配
if (custom_ctx->vlan_poped == 1) { // vlan pop
struct rte_flow_item_vlan vlan = {0};
vlan.tci = custom_ctx->out_header.l2.vlan.vlan_id;
flexda_insert_rte_key(hw_key, -1, &vlan, sizeof(rte_flow_item_vlan ), RTE_FLOW_ITEM_TYPE_VLAN);
// 下发vlan eth_type key，Hydra自定义key类型使用匹配的插入key api，提供与Hydra匹配的长度和类型枚举值
uint16_t
encap = custom_ctx->out_header.l2.vlan.encap;
flexda_insert_hydra_key(hw_key, -1, &encap, sizeof(uint16_t), HYDRA_ITEM_MYMAINPIPE_INNER_ETH_TYPE);
/*** 匹配Hydra数据面vlan pop场景硬件流表的其他组key代码 ***/
return 0;
}
/*** 其他场景卸载硬件流表组key代码 ***/
return -EINVAL;
}
//注：此处调用时hw_action已为流表归一拼接后的action，
// 参见以下示例代码：当处理VLAN RX报文时（vlan_poped为1），调用接口在hw_action中加入Hydra支持的RSS action（HWOFF_FLOW_ACT_DEF_INNER_RSS）。
int flexda_custom_construct_action(void *ctx, flexda_custom_info_t info, struct rte_flow_action *hw_action)
{
struct custom_packet_ctx *custom_ctx = (struct custom_packet_ctx *)ctx;
// 根据上下文条件控制组action流程，需要保证下发Hydra里table支持的action
if (custom_ctx->vlan_poped == 1) {
uint8_t rss = 1;
// Hydra自定义action类型使用匹配的插入action api，提供与Hydra匹配的长度和类型枚举值
flexda_insert_hydra_action(hw_action, -1, &rss, sizeof(uint8_t), DEFAULT_FLOW_ACTION_TYPE_INNER_RSS);
}
/*** 其他action编辑代码 ***/
return 0;
}
```


6. 可实现对OVS隧道端口的卸载请求进行放通。

```
int flexda_custom_check_tunnel(const char *devname)
{
// 当OVS卸载请求来自名称为gre的隧道端口时，可以卸载
if (strcmp(devname, "gre") == 0) {
return 0;
}
// 来自其他（非gre/vxlan/geneve/tap的端口）时不卸载
return -EINVAL;
}
```


#### 步骤二：编译构建

1. 如有需要，打包OVS控制面开发目录并发送至编译构建环境。
2. 编写OVS控制面CMake配置文件，引入src_control_plane目录下的头文件以及源文件，并设置产物名为custom_openvswitch_plugin.so。
3. 确认编译环境 DPDK 版本是否正确。DPDK
  版本约束可参见表2
  。

```
pkg-config --modversion libdpdk
```


4. 确认编译环境操作系统和架构是否正确。
  操作系统约束可参见表1
  。

```
uname -r
```


5. 执行编译构建。

```
cd {project_path}
mkdir build; cd build; cmake ..; make;
```


6. build目录下获取编译产物custom_openvswitch_plugin.so。


#### 步骤三：安装编译产物

1. 将获取的产物custom_openvswitch_plugin.so上传并复制到运行环境的“/usr/lib64”目录下。
2. 修改执行权限为550。

- 安装或更新custom_openvswitch_plugin.so后需要重启OVS软件生效。
- 用户需要将custom_openvswitch_plugin.so设置为root权限，否则libhiflexda_ovs_adapter.so在加载该动态库时可能由于权限校验不通过，导致加载失败。
- 推荐用户将custom_openvswitch_plugin.so的权限设置为550。权限高于550会增加被篡改的风险。
- 不建议在custom_openvswitch_plugin.so使用软链接，以免引入权限风险或访问受限。


#### 特殊场景说明

以示例代码ovs_example.zip为例（用户需自行准备）。根据以下操作执行控制面开发。

1. 在flexda_sdk目录下解压示例代码。
  目录结构如下所示，此处以geneve_single为例。

```
flexda_sdk/example/geneve_single/
├── CMakeLists.txt                   # 顶层CMake配置文件
└── src_control_plane                # 用户控制面源代码子目录
├── CMakeLists.txt               # 控制面子目录CMake配置文件
├── context.c                    # 控制面C语言源代码
└── context.h                    # 控制面C语言头文件
└── src_dsl                          # 用户数据面源代码子目录
├── CMakeLists.txt               # 数据面的子目录CMake配置文件
└── geneve_ovs.hdr               # 数据面Hydra源文件
```


2. 参见特殊场景说明执行固件编译。
3. 复制固件编译生成的hydra_info.h头文件到src_control_plane下。

```
cd {$sdk_path}/flexda_sdk/example/geneve_single/
cp src_dsl/build/hydra_info.h src_control_plane/
```


4. （可选）当前环境（固件编译环境）与OVS控制面编译环境不同时，将flexda_sdk打包，从当前环境发送至OVS控制面编译环境。

```
#当前环境执行：
zip -r flexda_sdk.zip {$sdk_path}/flexda_sdk
scp flexda_sdk.zip {$username}@{$cplane_compile_env_address}:{$sdk_path}/
#发送成功后登录至OVS控制面编译环境执行：
cd {$sdk_path}
unzip flexda_sdk.zip
cd flexda_sdk/example/geneve_single/
```


5. 进入OVS控制面项目目录，执行编译构建。

```
cd src_control_plane
# 保证cmake/make生成的目录和文件权限不会过大
umask 077
# 生成构建文件；
mkdir build; cd build; cmake ..;
# 执行make命令，进行动态库编译
make
```


  打印示例如下，表示编译成功。

```
[100%] Built target custom_openvswitch_plugin
```


6. 将编译生成的动态链接库安装至运行环境。

```
scp custom_openvswitch_plugin.so {$username}@{$run_env_address}:{$path}/
#登录至运行环境，安装动态链接库：
chmod 550 custom_openvswitch_plugin.so
cp {$path}/custom_openvswitch_plugin.so /usr/lib64
```
