CNI插件编写框架分析
概述
在《CNI, From A Developer's Perspective》一文中,我们已经对CNI有了较为深入的了解。我们知道,容器网络功能的实现最终是通过CNI插件来完成的。每个CNI插件本质上就是一个可执行文件,而CNI的执行流程无非就是从容器管理系统和配置文件获取配置信息,然后将这些信息以环境变量和标准输入的形式传输给插件,再运行插件完成具体的容器网络配置,最后将配置结果通过标志输出返回。
在我们对CNI的各种插件做了一个初步的浏览之后,我们会发现,虽然各个CNI插件实现容器网络的方式是多种多样的,但是它们编写的套路基本是一致的。其中一定会存在三个函数:main(),cmdAdd(),cmdDel()。接着我们回想一下《CNI, From A Developer's Perspective》一文中的描述,CNI其实就只有两个基本操作ADD和DEL,前者用于加入容器网络,后者用于从容器网络中退出。由此,通过上述三个函数,再加上一些合理的联想,我们也就不难勾勒出插件的执行流程了。当CNI插件被调用时,首先进入main函数,main函数会对环境变量和标准输入中的配置信息进行解析,接着根据解析得到的操作方式(ADD或DEL),转入具体的执行函数完成网络的配置工作。如果是ADD操作,则调用cmdAdd()函数,反之,如果是DEL操作,则调用cmdDel()函数。从宏观角度来看,CNI插件的实现框架是就是这样简单清晰。下面我们就以CNI官方插件库的bridge插件为例,深入上述三个函数的源码,来进一步说明CNI插件应该如何实现的。
(bridge插件源码链接:https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge)
main函数
1、main函数非常简单,仅仅只是调用了skel.PluginMain这个函数,并且将函数cmdAdd和cmdDel以及支持插件支持的CNI版本作为参数传递给它。
func main() {
skel.PluginMain(cmdAdd, cmdDel, version.All)
}
2、PluginMain函数是一个包裹函数,它直接对PluginMainWithError进行调用,当有错误发生的时候,会将错误以json的形式输出到标准输出,并退出插件的执行。
func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil {
if err := e.Print(); err != nil {
log.Print("Error writing error JSON to stdout: ", err)
}
os.Exit(1)
}
}
3、PluginMainWithError函数也非常简单,其实就是用环境变量,标准输入输出构造了一个dispatcher结构,再执行其中的pluginMain方法而已。
func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
return (&dispatcher{
Getenv: os.Getenv,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}).pluginMain(cmdAdd, cmdDel, versionInfo)
}
dispatcher结构如下所示:
type dispatcher struct {
Getenv func(string) string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer ConfVersionDecoder version.ConfigDecoder
VersionReconciler version.Reconciler
}
4、接着dispatcher结构的pluginMain方法执行具体的操作。该函数的操作分为如下两步:
- 首先调用cmd, cmdArgs, err := t.getCmdArgsFromEnv()从环境变量和标准输入中解析出操作信息cmd和配置信息cmdArgs
- 接着根据操作信息cmd的不同,调用checkVersionAndCall(),该函数会首先从标准输入中获取配置信息中的CNI版本,再和之前main函数中指定的插件支持的CNI版本信息进行比对。如果版本匹配,则调用相应的回调函数cmdAdd或cmdDel并以cmdArgs作为参数,否则,返回错误
func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
cmd, cmdArgs, err := t.getCmdArgsFromEnv()
.....
switch cmd {
case "ADD":
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
case "DEL":
......
}
......
}
5、下面我们来看看dispatcher的getCmdArgsFromEnv()方法是如何从环境变量和标准输入中获取配置信息的。首先来看一下cmdArgs的具体结构:
type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string
Path string
StdinData []byte
}
分析了上述结构之后我们可以发现CmdArgs中的内容和《CNI, From A Developer's Perspective》中描述的从容器管理系统中获取的运行时系统基本是一致的,而已知这些参数是通过环境变量传递给插件的。因此,不难想象,getCmdArgsFromEnv()所做的工作就是从环境变量中提取出配置信息用以填充CmdArgs,再将容器网络的配置信息,也就是标准输入中的内容,存入StdinData字段。具体代码如下所示:
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
var cmd, contID, netns, ifName, args, path string vars := []struct {
name string
val *string
reqForCmd reqForCmdEntry
}{
{
"CNI_COMMAND",
&cmd,
reqForCmdEntry{
"ADD": true,
"DEL": true,
},
},
....
{
"CNI_NETNS",
&netns,
reqForCmdEntry{
"ADD": true,
"DEL": false,
},
},
....
} argsMissing := false
for _, v := range vars {
*v.val = t.Getenv(v.name)
if *v.val == "" {
if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name)
argsMissing = true
}
}
} if argsMissing {
return "", nil, fmt.Errorf("required env variables missing")
} stdinData, err := ioutil.ReadAll(t.Stdin)
if err != nil {
return "", nil, fmt.Errorf("error reading from stdin: %v", err)
} cmdArgs := &CmdArgs{
ContainerID: contID,
Netns: netns,
IfName: ifName,
Args: args,
Path: path,
StdinData: stdinData,
}
return cmd, cmdArgs, nil
}
虽然getCmdArgsFromEnv()要完成的工作非常简单,但仔细分析代码之后,我们可以发现它的实现非常精巧。首先,它定义了一系列想要获取的参数,例如cmd,contID,netns等等。之后再定义了一个匿名结构的数组,匿名结构中包含了环境变量的名字,一个字符串指针(把该环境变量对应的参数赋给它,例如cmd对应CNI_COMMAND)以及一个reqForCmdEntry类型的成员reqForCmd。类型reqForCmdEntry其实是一个map,它在这里的作用是定义该环境变量是否为对应操作所必须的。例如,上文中的环境变量"CNI_NETNS",对于"ADD"操作为true,而对于"DEL"操作则为false,这说明在"ADD"操作时,该环境变量不能为空,否则会报错,但是在"DEL"操作时则无所谓。最后,遍历该数组进行参数的提取即可。
到此为止,main函数的任务完成。总的来说它做了三件事情:1、CNI版本检查,2、提取配置参数构建cmdArgs,3、调用对应的回调函数,cmdAdd或者cmdDel。
cmdAdd函数
1、如下所示cmdAdd函数一般分为三个步骤执行:
- 首先调用函数conf, err := loadNetConf(args.StdinData)(注:loadNetConf是插件自定义的,各个插件都不一样),从标准输入,也就是参数args.StdinData中获取容器网络配置信息
- 接着根据具体的配置信息进行网络的配置工作
- 最后,调用函数types.PrintResult(result, conf.CNIVersion)输出配置结果
func cmdAdd(args *skel.CmdArgs) error {
n, cniVersion, err := loadNetConf(args.StdinData)
......
return PrintResult(result, cniVersion)
}
2、接着我们对loadNetConf函数进行分析。因为每个CNI插件配置容器网络的方式各有不同,因此它们所需的配置信息一般也是不同的,除了大家共有的信息被包含在types.NetConf结构中,每个插件还定义了自己所需的字段。例如,对于bridge插件,它用于存储配置信息的结构如下所示:
type NetConf struct {
types.NetConf
BrName string `json:"bridge"`
IsGW bool `json:"isGateway"`
IsDefaultGW bool `json:"isDefaultGateway"`
ForceAddress bool `json:"forceAddress"`
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
}
而loadNetConf函数所做的操作也非常简单,就是调用json.Unmarshal(bytes, n)函数将配置信息从标准输入的字节流中解码到一个NetConf结构,具体代码如下:
func loadNetConf(bytes []byte) (*NetConf, string, error) {
n := &NetConf{
BrName: defaultBrName,
}
if err := json.Unmarshal(bytes, n); err != nil {
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
}
return n, n.CNIVersion, nil
}
3、最后,我们对配置结果的输出进行分析。由于不同的CNI版本要求的输出结果的内容是不太一样的,因此这部分内容其实是比较复杂的。下面我们就进入PrintResult函数一探究竟。
func PrintResult(result Result, version string) error {
newResult, err := result.GetAsVersion(version)
if err != nil {
return err
}
return newResult.Print()
}
从上面的代码中我们可以看出,该函数就做了两件事,一件是调用newResult, err := result.GetAsVersion(version),根据指定的版本信息,进行结果信息的版本转换。第二件就是调用newResult.Print()将结果信息输出到标准输出。
事实上,Result如下所示,是一个interface类型。每个版本的CNI都是定义了自己的Result结构的,而这些结构都是满足Result接口的。
// Result is an interface that provides the result of plugin execution
type Result interface {
// The highest CNI specification result verison the result supports
// without having to convert
Version() string // Returns the result converted into the requested CNI specification
// result version, or an error if conversion failed
GetAsVersion(version string) (Result, error) // Prints the result in JSON format to stdout
Print() error // Returns a JSON string representation of the result
String() string
}
而其中的GetAsVersion()方法则用于将当前版本的CNI Result信息转化到对应的CNI Result信息。我们来举个具体的例子,应该就很清晰了。
func (r *Result) GetAsVersion(version string) (types.Result, error) {
switch version {
case "0.3.0", ImplementedSpecVersion:
r.CNIVersion = version
return r, nil
case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]:
return r.convertTo020()
}
return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version)
}
假设现在我们的result的版本0.3.0, 但是插件要求返回的result版本是0.2.0的,根据上文中的代码,显然此时我们会调用r.convertTo020()函数进行转换,如下所示:
// Convert to the older 0.2.0 CNI spec Result type
func (r *Result) convertTo020() (*types020.Result, error) {
oldResult := &types020.Result{
CNIVersion: types020.ImplementedSpecVersion,
DNS: r.DNS,
} for _, ip := range r.IPs {
// Only convert the first IP address of each version as 0.2.0
// and earlier cannot handle multiple IP addresses
......
} for _, route := range r.Routes {
......
}
......
return oldResult, nil
}
该函数所做的操作,简单来说,就是定义了相应版本具体的Result结构,然后用当前版本的Result结构中的信息进行填充,从而完成Result版本的转化。
而Print方法对于各个版本的Result都是一样的,都是将Result进行json编码后,输出到标准输出而已。
到此为止,cmdAdd函数操作完成。
cmdDel函数
cmdDel和cmdAdd的执行结构是类似的,而且一般比cmdAdd还简单一些。同样,cmdDel先从args.Stdin中获取网络的配置信息,接着再进行相应的清理工作。最后,与cmdAdd不同的是,cmdDel不需要对结果进行输出,直接返回错误信息即可。
因为cmdDel和cmdAdd从结构层面来看是类似的,因此就不再赘述了。
结语
上文对CNI插件的执行框架进行了比较深入的分析。总的来说,一般插件的执行就是三部分内容:1、解析配置信息,2、执行具体的网络配置ADD或DEL,3、对于ADD操作还需输出结果。整体来说,架构还是非常简洁清晰的。
如果你有任何新的容器网络方案,希望通过本文的阅读可以让你迅速地编写出对应的CNI插件。
CNI插件编写框架分析的更多相关文章
- CNI插件实现框架---以loopback为示例
以最简单的loopback插件作为实例,来分析CNI plugin的执行流程 // cni/plugins/loopback/loopback.go 1.func main() main函数只是简单地 ...
- Android Small插件化框架源码分析
Android Small插件化框架源码分析 目录 概述 Small如何使用 插件加载流程 待改进的地方 一.概述 Small是一个写得非常简洁的插件化框架,工程源码位置:https://github ...
- CNI bridge 插件实现代码分析
对于每个CNI 插件在执行函数cmdAdd之前的操作是完全一样的,即从环境变量和标准输入内读取配置.这在http://www.cnblogs.com/YaoDD/p/6410725.html这篇博文里 ...
- CNI插件源码示例,对于github.com/rajatchopra/ocicni库的分析
CNI插件初始化 // ocicni.go 1.func InitCNI(pluginDir string) (CNIPlugin, error) (1).先调用plugin := probeNetw ...
- 几款开源的hybird移动app框架分析
几款开源的Hybrid移动app框架分析 Ionic Onsen UI 与 ionic 相比 jQuery Mobile Mobile Angular UI 结论 很多移动开发者喜欢使用原生代码开发, ...
- BugScan插件编写高(gǎo)级(jī)教程
声明:本文最先发布在:http://q.bugscan.net/t/353 转载请注明出处 有问题可以和我交流 邮件(Medici.Yan@gmail.com) 个人博客地址:http://www.c ...
- Android基于代理的插件化思路分析
前言 正常的App开发流程基本上是这样的:开发功能-->测试--->上线,上线后发现有大bug,紧急修复---->发新版本---->用户更新----->bug修复.从发现 ...
- Wireshark插件编写
Wireshark插件编写 在抓包的过程中学习了使用wireshark,同时发现wireshark可以进行加载插件,便在网上学习了一下相应的插件开发技术. 需求编写一个私有协议名为SYC,使用UDP端 ...
- Android Small插件化框架解读——Activity注册和生命周期
通过对嵌入式企鹅圈原创团队成员degao之前发表的<Android Small插件化框架源码分析>的学习,对Android使用的插件化技术有了初步的了解,但还是有很多需要认真学习的地方,特 ...
随机推荐
- [转]VC传递消息sendmessage
SendMessage的基本结构如下: SendMessage( HWND hWnd, //消息传递的目标窗口或线程的句柄. UINT Msg, //消息类别(这里可以是一些系统消息,也可以是自己定 ...
- Memcache内存分配策略
一.Memcache内存分配机制 关于这个机制网上有很多解释的,我个人的总结如下. Page为内存分配的最小单位. Memcached的内存分配以page为单位,默认情况下一个page是1M,可以通过 ...
- Redis学习笔记——简介及配置
1.Redis简介 Redis概述 Redis是一个开源,先进的key-value存储,并用于构建高性能,可扩展的应用程序的完美解决方案.Redis从它的许多竞争继承来的三个主要特点:Redis数据库 ...
- Spring mvc 返回JSON 在IE 下提示下载 解决办法
http://www.blogjava.net/iamlibo/archive/2013/11/21/406646.html ————————————————————————————————————— ...
- 重新=》easyui DataGrid是否可以动态的改变列显示的顺序
$.extend($.fn.datagrid.methods,{ columnMoving: function(jq){ return jq.each(function(){ var target = ...
- Could not contact Selenium Server; have you started it on 'localhost:4444'
今天学习selenium RC例子的时候遇到一个问题:java.lang.RuntimeException: Could not contact Selenium Server; have you s ...
- 【BZOJ】3398: [Usaco2009 Feb]Bullcow 牡牛和牝牛(排列组合+乘法逆元+欧拉定理/费马小定理)
http://www.lydsy.com/JudgeOnline/problem.php?id=3398 以下牡牛为a,牝牛为b. 学完排列计数后试着来写这题,“至少”一词可以给我们提示,我们可以枚举 ...
- 【基础练习】【BFS+A*】codevs1225八数码难题题解
题目描写叙述 Description Yours和zero在研究A*启示式算法.拿到一道经典的A*问题,可是他们不会做,请你帮他们. 问题描写叙述 在3×3的棋盘上,摆有八个棋子,每一个棋子上标有1至 ...
- LR的响应时间与使用IE所感受时间不一致的讨论(摘抄补充)
http://www.51testing.com/html/33/564333-865629.html 在做性能测试时,有时碰到这样一种情况,使用性能工具LR测试出来的响应时间比实际使用IE感受到的时 ...
- Android音视频学习第7章:使用OpenSL ES进行音频解码
/* * *这里使用了transcode-1.1.7对wav文件进行解码.然后使用opensl es进行播放 * */ //用到的变量和结构体 WAV wav; //wav文件指针 SLObjectI ...