95992828九五至尊2

CNI插件达成框架,CNI插件编写框架分析

三月 19th, 2019  |  882828九五至尊手机版

概述

以最简便易行的loopback插件作为实例,来分析CNI plugin的实践流程

在《CNI, From A Developer’s
Perspective》一文中,大家早已对CNI有了比较长远的垂询。大家通晓,容器网络功效的落实最终是通过CNI插件来成功的。每一种CNI插件本质上正是多个可执行文件,而CNI的履行流程无非就是从容器管理体系和陈设文件获取配置音讯,然后将这个消息以环境变量和正式输入的方式传输给插件,再运行插件完结具体的容器互连网布局,最终将配置结果通过标志输出重回。

// cni/plugins/loopback/loopback.go

在大家对CNI的各样插件做了3个方始的浏览之后,我们会发现,尽管种种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插件应该什么落到实处的。

1、func main()

(bridge插件源码链接:https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge)

main函数只是简短地调用skel.PluginMain(cmdAdd, cmdDel,
version.All),注册插件中的插入和删除方法

 

 

main函数

// cni/pkg/skel/skel.go

// PluginMain is the core "main" for a plugin which includes automatic error handling.

// The caller must also specify what CNI spec versions the plugin supports.

// When an error occurs in either cmdAdd or cmdDel, PluginMain will print the error

// as JSON to stdout and call os.Exit(1).

// To have more control over error handling, use PluginMainWithErro() instead.

① 、main函数十分不难,仅仅只是调用了skel.PluginMain这些函数,并且将函数cmdAdd和cmdDel以及帮忙插件援救的CNI版本作为参数字传送递给它。

2、func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error,
versionInfo version.PluginInfo)

func main() {
    skel.PluginMain(cmdAdd, cmdDel, version.All)
}

该函数仅仅调用e := PluginMainWithError(cmdAdd, cmdDel, versionInfo)

  

若e不为nil,调用e.Print()并os.Exit()

贰 、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)
    }
}

// cni/pkg/skel/skel.go

  

3、func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error,
versionInfo version.PluginInfo) *types.Error

三 、PluginMainWithError函数也非常不难,其实正是用环境变量,标准输入输出构造了二个dispatcher结构,再实施个中的pluginMain方法而已。

该函数仅仅调用return (&dispatcher{

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)
}

    Getenv:  os.Getenv,

  

    Stdin:   os.Stdin,

dispatcher结构如下所示:

    Stdout:   os.Stdout,

type dispatcher struct {
    Getenv func(string) string
    Stdin  io.Reader
    Stdout io.Writer
    Stderr io.Writer

    ConfVersionDecoder version.ConfigDecoder
    VersionReconciler  version.Reconciler
}

    Stderr:   os.Stderr,

  

}).pluginMain(cmdAdd, cmdDel, versionInfo)

四 、接着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":
                ......
    }
        ......
    

    }

dispatcher 结构如下所示:

  

type dispatcher struct {
  Getenv   func(string) string
  Stdin:   io.Reader
  Stdout:   io.Write
  Stderr:    io.Write

  ConfVersionDecoder  version.ConfigDecoder
  VersionReconciler    version.Reconciler
}

⑤ 、上面大家来探望dispatcher的getCmdArgsFromEnv()方法是怎样从环境变量和标准输入中获取配置新闻的。首先来看一下cmdArgs的具体组织:

 

type CmdArgs struct {
    ContainerID string
    Netns       string
    IfName      string
    Args        string
    Path        string
    StdinData   []byte
}

// cni/pkg/skel/skel.go

  

4、func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs)
error, versionInfo version.PluginInfo) *types.Error

分析了上述协会从此大家得以窥见CmdArgs中的内容和《CNI, From A Developer’s
Perspective》中讲述的从容器管理种类中取得的周转时系统宗旨是平等的,而已知那个参数是通过环境变量传递给插件的。因而,简单想象,getCmdArgsFromEnv()所做的办事正是从环境变量中提取出布局消息用于填充CmdArgs,再将容器互连网的安插新闻,也正是正规输入中的内容,存入StdinData字段。具体代码如下所示:

1、先调用cmd, cmdArgs, err := t.getCmdArgsFromEnv()解析出,操作指令(ADD
,DEL大概VE瑞鹰SION),和操作参数

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
}

二 、再依照不相同的cmd,调用相关的函数。对于”ADD”,调用t.checkVersionAndCall(cmdArgs,
versionInfo, cmdAdd)

  

 

虽说getCmdArgsFromEnv()要做到的干活万分简单,但仔细分析代码之后,大家可以窥见它的落到实处充裕精细。首先,它定义了一多种想要获取的参数,例如cmd,contID,netns等等。之后再定义了1个匿名结构的数组,匿名结构中富含了环境变量的名字,三个字符串指针(把该环境变量对应的参数赋给它,例如cmd对应CNI_COMMAND)以及八个reqForCmdEntry类型的分子reqForCmd。类型reqForCmdEntry其实是几个map,它在此地的意义是概念该环境变量是还是不是为相应操作所必须的。例如,上文中的环境变量”CNI_NETNS”,对于”ADD”操作为true,而对于”DEL”操作则为false,那表达在”ADD”操作时,该环境变量不可能为空,不然会报错,可是在”DEL”操作时则无所谓。最终,遍历该数组进行参数的提取即可。

// cni/pkg/skel/skel.go

到此结束,main函数的职分到位。总的来说它做了三件工作:一 、CNI版本检查,② 、提取配置参数营造cmdArgs,③ 、调用对应的回调函数,cmdAdd恐怕cmdDel。

5、func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs,
error)

 

该函数从环境变量和stdin中领到消息,填充数据结构CmdArgs,如下所示:

cmdAdd函数

cmdArgs := &CmdArgs{
  ContainerID:    contID,
  Netns:        netns,
  IfName:       ifName,
  Args:        args,
  Path:        path,
  StdinData:     stdinData,    // StdinData中的数据其实是network配置
}

一 、如下所示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)
    

    }

// cni/pkg/skel/skel.go

  

6、func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs,
pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error)
error

② 、接着大家对loadNetConf函数实行解析。因为每种CNI插件配置容器网络的方法各有不一致,由此它们所需的布局消息一般也是见仁见智的,除了大家共有的音信被含有在types.NetConf结构中,每种插件还定义了友好所需的字段。例如,对于bridge插件,它用于存款和储蓄配置消息的构造如下所示:

壹 、调用confiVersion, err :=
t.ConfVersionDecoder.Decode(cmdArgs.StdinData)获取互连网布局中的版本

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"`
}

2、调用verErr := t.VersionReconciler.Check(configVersion,
pluginVersionInfo)

  

叁 、最终调用return toCall(cmdArgs)

而loadNetConf函数所做的操作也非凡简单,正是调用json.Unmarshal(bytes,
n)函数将安排音信从专业输入的字节流中解码到3个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
}

// cni/plugins/loopback/loopback.go

  

7、func cmdAdd(args *skel.CmdArgs) error

③ 、最后,大家对配备结果的输出进行剖析。由于分歧的CNI版本要求的出口结果的始末是不太相同的,由此那部分内容其实是比较复杂的。上边我们就进来PrintResult函数一切磋竟。

壹 、因为loopback比较尤其,因而一向忽略args,设置args.IfName = “lo”

func PrintResult(result Result, version string) error {
    newResult, err := result.GetAsVersion(version)
    if err != nil {
        return err
    }
    return newResult.Print()
}

2、调用ns.WithNetNSPath(args.Netns, do func(_ ns.NetNS)
error),在args钦点的net ns中施行函数do

  

三 、loopback相比不难,net ns中的执行函数只是调用link, err :=
netlink.LinkByName(args.IfName)找到lo设备,再调用netlink.LinkSetUp(link)运转而已

从上面包车型地铁代码中大家得以见到,该函数就做了两件事,一件是调用newResult, err
:=
result.GetAsVersion(version),依照内定的版本消息,举办结果音信的本子转换。第叁件正是调用newResult.Print()将结果音信输出到专业输出。

4、调用result := current.Result{}并return result.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
}

综上完结CNI插件的实践流程

  

 

而里面包车型大巴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插件的执行框架进行了相比较深切的剖析。总的来说,一般插件的实施就是三局地情节:一 、解析配置消息,二 、执行实际的网络布局ADD或DEL,③ 、对于ADD操作还需出口结果。全体来说,架构依旧要命简洁清晰的。

要是你有任何新的容器互联网方案,希望通过本文的阅读能够让你火速地编写出对应的CNI插件。

相关文章

Your Comments

近期评论

    功能


    网站地图xml地图