控制面开发详细步骤
开发前准备
- 开发者可以在自定义代码工程下创建OVS控制面开发目录。示例如下,详情请参见自定义目录定制代码。
ovs_project/ ├── CMakeLists.txt # 顶层CMake配置文件 ├── src_control_plane # 用户OVS控制面源代码目录 ├── CMakeLists.txt # OVS控制面CMake配置文件 └── ... # OVS控制面源代码 └── src_dsl # 用户数据面源代码子目录 ├── CMakeLists.txt # 数据面的子目录CMake配置文件 └── ovs_project.hdr # 数据面Hydra源文件 - 复制固件编译生成的hydra_info.h头文件到 src_control_plane下。详情请参见执行固件编译 。
cd {$project_path}/ovs_project cp src_dsl/build/hydra_info.h src_control_plane/ - 在代码中引入编程框架提供的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启动不成功。
- 开发代码前根据业务场景以及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
HYDRA_ITEM_MYMAINPIPE_INNER_ETH_TYPE
- 根据需要,定义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; } - 根据业务场景需要,定义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; } - 根据业务场景,在更新上下文和提取报文头的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 }
- 根据业务场景和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类型使用匹配的插入key api,提供与Hydra匹配的长度和类型枚举值 flexda_insert_hydra_action(hw_action, -1, &rss, sizeof(uint8_t), DEFAULT_FLOW_ACTION_TYPE_INNER_RSS); } /*** 其他action编辑代码 ***/ return 0; } - 可实现对OVS隧道端口的卸载请求进行放通。
int flexda_custom_check_tunnel(const char *devname) { // 当OVS卸载请求来自名称为gre的隧道端口时,可以卸载 if (strcmp(devname, "gre") == 0) { return 0; } // 来自其他(非gre/vxlan/geneve/tap的端口)时不卸载 return -EINVAL; }
步骤二:编译构建
- 如有需要,打包OVS控制面开发目录并发送至编译构建环境。
- 编写OVS控制面CMake配置文件,引入src_control_plane目录下的头文件以及源文件,并设置产物名为custom_openvswitch_plugin.so。
- 确认编译环境DPDK版本是否正确。DPDK版本约束请参见表2。
pkg-config --modversion libdpdk
- 确认编译环境操作系统和架构是否正确。操作系统约束请参见表1。
uname -r
- 执行编译构建。
cd {project_path} mkdir build; cd build; cmake ..; make; - build目录下获取编译产物custom_openvswitch_plugin.so。
步骤三:安装编译产物
- 将获取的产物custom_openvswitch_plugin.so上传并复制到运行环境的“/usr/lib64”目录下。
- 修改执行权限为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为例(用户需自行准备)。请根据以下操作执行控制面开发。
- 在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源文件 - 参考特殊场景说明执行固件编译。
- 复制固件编译生成的hydra_info.h头文件到src_control_plane下。
cd {$sdk_path}/flexda_sdk/example/geneve_single/ cp src_dsl/build/hydra_info.h src_control_plane/ - (可选)当前环境(固件编译环境)与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/ - 进入OVS控制面项目目录,执行编译构建。
cd src_control_plane # 保证cmake/make生成的目录和文件权限不会过大 umask 077 # 生成构建文件; mkdir build; cd build; cmake ..; # 执行make命令,进行动态库编译 make
打印示例如下所示,表示编译成功。[100%] Built target custom_openvswitch_plugin
- 将编译生成的动态链接库安装至运行环境。
scp custom_openvswitch_plugin.so {$username}@{$run_env_address}:{$path}/ #登录至运行环境,安装动态链接库: chmod 550 custom_openvswitch_plugin.so cp {$path}/custom_openvswitch_plugin.so /usr/lib64
父主题: 开发控制面实践示例