源码研习之 CNI

CNI(第二篇): Container Network Interface - networking for Linux containers

Posted by 董江 on Friday, January 17, 2020

CNI(第二篇)

介绍

CNI(容器网络接口)是一个规范SPEC。 如何配置调用: 请查看CNI SPEC

CNI包括几部分:

golang SDK Lib, 用于集成实现网络通信接口;

Template,用于生成自定义的CNI插件,标准代码工程;

标准Document,包括社区公约、描述、roadmaps、milestone等

使用方式

cnitool 介绍

cnitool是将已经编译完成的的容器网络插件Adapter,添加到VM中或者PM中。

const (
	EnvCNIPath        = "CNI_PATH"   //CNI Adapter bin文件路径(bin的编译需要去掉cgo)
	EnvNetDir         = "NETCONFPATH"  //部署(添加、删除、检查)adapter需要的json配置文件路径
	EnvCapabilityArgs = "CAP_ARGS"  // CAP 参数
	EnvCNIArgs        = "CNI_ARGS"  // adapter 外部传递的参数,一般不用,将args放在json文件中
	EnvCNIIfname      = "CNI_IFNAME"  // 设置的容器网卡名称,如eth0

	DefaultNetDir = "/etc/cni/net.d" // 默认CNI 插件路径

	// 以下部分是 插件 添加方式
	CmdAdd   = "add"
	CmdCheck = "check"
	CmdDel   = "del"
)

插件使用

准备二进制插件:

go get github.com/containernetworking/plugins
go build ptp -o myptp

准备网络配置:

echo `
{
	"cniVersion": "0.4.0",
	"name": "myptp",
	"type": "ptp",  //veth pair
	"ipMasq": true,
	"ipam": {
		"type": "host-local",
		"subnet": "172.16.29.0/24",
		"routes": [{
			"dst": "0.0.0.0/0"
		}]
	}
}` > /etc/cni/net.d/myptp.conflist

准备好配置后部署容器网络:

第一步:创建网络Namespace, 添加名称为mytest_network

sudo ip netns add mytest_network

第二步:添加容器网络myptp

sudo CNI_PATH=/etc/cni/net.d/ cnitool add myptp /var/run/netns/mytest_network

第三步:调测网络

sudo ip -n mytest_network addr
sudo ip netns exec mytest_network ping -c 1 4.2.2.2

最后,清理网络

sudo CNI_PATH=./etc/cni/net.d/ cnitool del myptp /var/run/netns/mytest_network
sudo ip netns del mytest_network

cnitool调用插件

正如上面的例子:cnitool add myptp /var/run/netns/mytest_network

参数0是cnitool工具名称 参数1是操作名称(add、del、check) 参数3是加载的配置对象 myptp.* 文件对象 参数4是 net namespace 名称

解析过程:

import (
	...
	"github.com/containernetworking/cni/libcni"  // golang lib SDK
)

func main() {
	if len(os.Args) < 4 {  // 判断参数
		usage()
		return
	}

	netdir := os.Getenv(EnvNetDir)  //加载conf文件地址
	if netdir == "" {
		netdir = DefaultNetDir
	}
	netconf, err := libcni.LoadConfList(netdir, os.Args[2])
	if err != nil {
		exit(err)
	}

	... 


	ifName, ok := os.LookupEnv(EnvCNIIfname) // 加载网卡
	if !ok {
		ifName = "eth0"
	}

	...

	netns := os.Args[3]    //获得 new working namespace
	netns, err = filepath.Abs(netns)
	if err != nil {
		exit(err)
	}

	...

	// CNI runtime object
	cninet := libcni.NewCNIConfig(filepath.SplitList(os.Getenv(EnvCNIPath)), nil)
	rt := &libcni.RuntimeConf{
		ContainerID:    fmt.Sprintf("cnitool-%x", ha512.Sum512([]byte(netns))[:10]),
		NetNS:          netns,
		IfName:         ifName,
		Args:           cniArgs,
		CapabilityArgs: capabilityArgs,
	}

	...

	// CNI 具体执行方式
	switch os.Args[1] {
	case CmdAdd:
		result, err := cninet.AddNetworkList(context.TODO(), netconf, rt)
		if result != nil {
			_ = result.Print()
		}
		exit(err)
	case CmdCheck:
		err := cninet.CheckNetworkList(context.TODO(), netconf, rt)
		exit(err)
	case CmdDel:
		exit(cninet.DelNetworkList(context.TODO(), netconf, rt))
	}
}

Golang libcni SDK

SDK核心的interface 接口定义:

import(
	"github.com/containernetworking/cni/pkg/invoke" //syscall 具体调用
	"github.com/containernetworking/cni/pkg/types" // 网络adapter管理,使用Plugin Chains模型进行关联
	"github.com/containernetworking/cni/pkg/utils" // 公共方法:cni conf file中各个字段ValidCheck
	"github.com/containernetworking/cni/pkg/version" // version compare相关: 0.2.x以下版本兼容;0.3.x 和 现在的0.4.x 版本兼容
)
type CNI interface {
	AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
	CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
	GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

	//以下3个最核心接口
	AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
	CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
	DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error

	GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
	GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

	ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
	ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}

type CNIConfig struct {
	Path     []string
	exec     invoke.Exec
	cacheDir string
}

var _ CNI = &CNIConfig{} // *注意,将CNIConfig{}对象指针都是CNI接口的实现实例;

核心接口分析

func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
	c.ensureExec()  // 添加数据平台
	pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
	if err != nil {
		return nil, err
	}
	if err := utils.ValidateContainerID(rt.ContainerID); err != nil {
		return nil, err
	}
	if err := utils.ValidateNetworkName(name); err != nil {
		return nil, err
	}
	if err := utils.ValidateInterfaceName(rt.IfName); err != nil {
		return nil, err
	}

	newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
	if err != nil {
		return nil, err
	}

	return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec) //执行了Plugin 注入os.Exec
}

type RawExec struct {
	Stderr io.Writer
}

func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
	stdout := &bytes.Buffer{}
	c := exec.CommandContext(ctx, pluginPath)
	c.Env = environ
	c.Stdin = bytes.NewBuffer(stdinData)
	c.Stdout = stdout
	c.Stderr = e.Stderr
	if err := c.Run(); err != nil {
		return nil, pluginErr(err, stdout.Bytes())
	}

	return stdout.Bytes(), nil
}

func pluginErr(err error, output []byte) error {
	if exitError, ok := err.(*exec.ExitError); ok {
		emsg := types.Error{}
		if len(output) == 0 {
			if len(exitError.Stderr) == 0 {
				emsg.Msg = "netplugin failed with no error message"
			} else {
				emsg.Msg = fmt.Sprintf("netplugin failed: %q", string(exitError.Stderr))
			}
		} else if perr := json.Unmarshal(output, &emsg); perr != nil {
			emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
		}
		return &emsg
	}

	return err
}

func (e *RawExec) FindInPath(plugin string, paths []string) (string, error) {
	return FindInPath(plugin, paths)
}

将数据接入到Os kernel数据

「如果这篇文章对你有用,请随意打赏」

Kubeservice博客

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付