标签: NFV

  • mininet 学习

    命令语法

    $ 这个符号代表现在处于 Linux 的shell 交互下,需要使用的是 Linux 命令 mininet> 这个符号表示现在处于 Mininet 交互下,需要使用的是 Mininet 的命令 # 这个符号表示的是现在处于 Linux 的 root 权限下。 sudo mn -h 命令用于显示mininet的帮助信息 Mininet使用基于过程虚拟化和网络命名空间的特性来创建虚拟网络,并且创建的网络在当前的Linux内核中是可用的

    sudo mn 启动Mininet

    查看全部节点:nodes

    查看链路信息:net

    输出各节点信息:dump

    sudo mn –test pingpair 直接对主机连通性进行测试

    sudo mn –test iperf启动后直接进行性能测试

    主机端

    sudo mn -x ,通过使用-x参数,Mininet在启动后会在每个节点上自动打开一个XTerm,方便某些情况下对多个节点分别进行操作。

    在进入mn cli 之后,也可以使用xterm node(s1 h2)命令指定启动某些节点上的xterm,如分别启动s1和h2上的xterm

    禁用或启用某条链路,格式为:link node1 node2 up/down

    –switch选项和–controller选项,可以指定采用哪种类型的交换机跟控制器

    –innamespace参数,可以让所有结点拥有各自的名字空间

    启动参数总结 -h, –help 打印帮助信息

    –switch=SWITCH 交换机类型,包括 [kernel user ovsk]

    –host=HOST 模拟主机类型,包括 [process]

    –controller=CONTROLLER 控制器类型,包括 [nox_dump none ref remote nox_pysw]

    –topo=TOPO,arg1,arg2,…argN 指定自带拓扑,包括 [tree reversed single linear minimal]

    -c, –clean清理环境

    –custom=CUSTOM 使用自定义拓扑和节点参数

    –test=TEST 测试命令,包括 [cli build pingall pingpair iperf all iperfudp none]

    -x, –xterms 在每个节点上打开 xterm

    –mac 让MAC 地址跟 DP ID 相同

    –arp 配置所有 ARP 项

    -v VERBOSITY, –verbosity=VERBOSITY [info warning critical error debug output] 输出日志级别

    –ip=IP 远端控制器的IP地址

    –port=PORT 远端控制器监听端口

    –innamespace 在独立的名字空间内

    –listenport=LISTENPORT 被动监听的起始端口

    –nolistenport 不使用被动监听端口

    –pre=PRE 测试前运行的 CLI 脚本

    –post=POST 测试后运行的 CLI 脚本

    常用命令总结 help 默认列出所有命令文档,后面加命令名将介绍该命令用法

    dump 打印节点信息

    gterm 给定节点上开启 gnome-terminal。注:可能导致 Mininet 崩溃

    xterm 给定节点上开启 xterm

    intfs 列出所有的网络接口

    iperf 两个节点之间进行简单的 iperf TCP测试

    iperfudp 两个节点之间用指定带宽 udp 进行测试

    net 显示网络链接情况

    noecho 运行交互式窗口,关闭回应(echoing)

    pingpair 在前两个主机之间互 ping 测试

    source 从外部文件中读入命令

    dpctl 在所有交换机上用 dptcl 执行相关命令,本地为 tcp 127.0.0.1:6634

    link 禁用或启用两个节点之间的链路

    nodes 列出所有的节点信息

    pingall 所有 host 节点之间互 ping

    py 执行 Python 表达式

    sh 运行外部 shell 命令

    quit/exit 退出

  • LXD源码解析

    工作中忙的项目和lxd打交道比较多,因此我利用闲暇时间阅读了一下lxd的源码,以加深对于lxd的理解,顺便学习一些写golang的技巧。

    关于lxd

    lxd是lxc的第二版,和docker类似,也是一个利用Linux容器的管理工具。Linux容器可以实现一个类似与Linux虚拟机类似的环境,不同点是,牺牲了一定的隔离性的情况下运行开销更低。

    而lxd相较于lxc来说,相当于在管理方式上进行了一层封装。lxc的配置文件完全依赖人工编写,支持的存储后端只有dir(也就是在原有系统存储中的目录),网络的管理方式也非常匮乏,基本上只能使用手动配合外部工具才能有效地利用容器。而lxd,也就是官方所说的2.0,在众多方面都做出了改进。

    1. 全新的C/S架构。客户端为一个名字叫lxc的工具(注意这个和原先那个lxc不是一个东西),服务端叫lxd,两者之间可以使用unix socket或者https的方式,通过RESTful API进行通信。这种设计让用户可以利用lxc工具对多个lxd进行远程管理,更重要的是,第三方程序也可以完全不依赖与lxc工具,直接使用API对lxd进行管理。相比lxc而言这种管理方式灵活了许多。
    2. 更方便的配置项。lxd提出了许多的新概念,可以让lxc容器的配置显得更加条理化。例如,lxd引入了镜像库,镜像库可以是在本地也可以在远程,支持镜像在两者之间的转移、导出、导入,也可以将停止运行的容器打包为镜像;lxd引入了profile的概念,profile在容器创建时被指定,而基于同一个profile的容器在初始化时具有同样的配置参数。
    3. 更加丰富的存储后端。lxd除了原有的dir类型外,还支持一些现代的高效存储后端,例如btrfs、zfs、ceph等,只需要在启用时安装配套工具即可。
    4. 原生的网络配置。单个容器能够发挥的功能十分有限,如何将多个容器进行连接是非常关键的。lxd直接融合了多种网络的配置功能,例如创建bridge、ovs、veth,以及overlay类型的接口GRE、Vxlan以及Ubuntu fan等等。
    5. 更加方便的设备管理。对于宿主机上的物理资源,lxd也直接提供了device的配置方法,用户可以按照需求直接将多种类型的设备绑定到容器中,例如GPU、物理网络接口、磁盘、infiniteband以及其他的字符型设备和块设备等。
    6. 自带的集群模式。可以将多个运行lxd的服务器组合为集群进行管理,数据一致性由raft保证,这样可以提高lxd的稳定性。

    此外,lxd还提供了容器的迁移,并且在保证这些特性的同时,原有的lxc配置参数在lxd中得以保留。可以说,lxd的出现极大地提高了用户管理lxc的灵活度。

    lxc

    这里的lxc特指lxd的客户端,在lxd源码中的位置是lxc。这里额外提一下,在lxd(也就是lxc 2.0)中,一般使用的工具是lxc,运行的命令一般是lxc startlxc stop等等;而在lxc(也就是lxc 1.0)中,一般使用的工具是lxc-***,运行的命令一般是lxc-startlxc-stop。由于这里lxc经常出现,注意区分不要弄错了。

    系统 服务名 开启容器的命令
    lxc lxc lxc-start
    lxd lxd lxc start

    golang的程序一般是从包中的main函数开始的,因此这里首先看lxc/main.go文件。

    func main() {
    	// 定位配置文件,从配置文件中获得一些预制的运行参数
        err := execIfAliases()
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    		os.Exit(1)
    	}
    
    	// 配置解析器
    	app := &cobra.Command{}
    	app.Use = "lxc"
    	app.Short = i18n.G("Command line client for LXD")
    	app.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
    		`Command line client for LXD
    
    All of LXD's features can be driven through the various commands below.
    For help with any of those, simply call them with --help.`))
    	app.SilenceUsage = true
    	app.SilenceErrors = true
    
    	// Global flags
        globalCmd := cmdGlobal{cmd: app}
        // 添加全局对象的处理逻辑
    	app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, i18n.G("Print version number"))
    	app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, i18n.G("Print help"))
    	app.PersistentFlags().BoolVar(&globalCmd.flagForceLocal, "force-local", false, i18n.G("Force using the local unix socket"))
    	app.PersistentFlags().StringVar(&globalCmd.flagProject, "project", "", i18n.G("Override the source project"))
    	app.PersistentFlags().BoolVar(&globalCmd.flagLogDebug, "debug", false, i18n.G("Show all debug messages"))
    	app.PersistentFlags().BoolVarP(&globalCmd.flagLogVerbose, "verbose", "v", false, i18n.G("Show all information messages"))
    	app.PersistentFlags().BoolVarP(&globalCmd.flagQuiet, "quiet", "q", false, i18n.G("Don't show progress information"))
    
        // Wrappers
        // 配置运行前后的钩子函数
    	app.PersistentPreRunE = globalCmd.PreRun
    	app.PersistentPostRunE = globalCmd.PostRun
    
    	// Version handling
    	app.SetVersionTemplate("{{.Version}}\n")
    	app.Version = version.Version
    
    	// alias sub-command
    	aliasCmd := cmdAlias{global: &globalCmd}
    	app.AddCommand(aliasCmd.Command())
    
    	// cluster sub-command
    	clusterCmd := cmdCluster{global: &globalCmd}
    	app.AddCommand(clusterCmd.Command())
    
        // ... 中间这部分和alias,cluster一样,都是在绑定子命令的入口
        
    	// version sub-command
    	versionCmd := cmdVersion{global: &globalCmd}
    	app.AddCommand(versionCmd.Command())
    
    	// Get help command
    	app.InitDefaultHelpCmd()
    	var help *cobra.Command
    	for _, cmd := range app.Commands() {
    		if cmd.Name() == "help" {
    			help = cmd
    			break
    		}
    	}
    
    	// Help flags
    	app.Flags().BoolVar(&globalCmd.flagHelpAll, "all", false, i18n.G("Show less common commands"))
    	help.Flags().BoolVar(&globalCmd.flagHelpAll, "all", false, i18n.G("Show less common commands"))
    
    	// Deal with --all flag
    	err = app.ParseFlags(os.Args[1:])
    	if err == nil {
    		if globalCmd.flagHelpAll {
    			// Show all commands
    			for _, cmd := range app.Commands() {
    				cmd.Hidden = false
    			}
    		}
    	}
    
    	// Run the main command and handle errors
    	err = app.Execute()
    	if err != nil {
    		// Handle non-Linux systems
    		if err == config.ErrNotLinux {
    			fmt.Fprintf(os.Stderr, i18n.G(`This client hasn't been configured to use a remote LXD server yet.
    As your platform can't run native Linux containers, you must connect to a remote LXD server.
    
    If you already added a remote server, make it the default with "lxc remote switch NAME".
    To easily setup a local LXD server in a virtual machine, consider using: https://multipass.run`)+"\n")
    			os.Exit(1)
    		}
    
    		if err == cobra.ErrSubCommandRequired {
    			os.Exit(0)
    		}
    
    		// Default error handling
    		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    		os.Exit(1)
    	}
    
    	if globalCmd.ret != 0 {
    		os.Exit(globalCmd.ret)
    	}
    }
    
    

    根据我的使用经验,lxc这个客户端是唯一一个能够与交换机终端媲美的一个客户端程序。我们都知道很多有众多命令参数的程序如果不阅读手册是完全没法使用的,而lxc无论在任何时候子命令状态下,都可以通过不敲后面的参数来获取帮助,而且说明信息非常的详细,只有在某些特定的参数单位不太清楚时才需要查阅手册。这部分源码可以说是显示出它神奇的奥秘。原来,lxc使用了一个名字叫cobra的库,这个库可以对命令行参数进行非常华丽的处理。这段逻辑中我们唯独需要关注的,就是一开头的execIfAliases,这个函数主要就是定位lxd使用的config.yml文件,然后从文件中读取配置信息,填充到全局使用的结构体cmdGlobal中。

    type cmdGlobal struct {
    	conf     *config.Config
    	confPath string
    	cmd      *cobra.Command
    	ret      int
    
    	flagForceLocal bool
    	flagHelp       bool
    	flagHelpAll    bool
    	flagLogDebug   bool
    	flagLogVerbose bool
    	flagProject    string
    	flagQuiet      bool
    	flagVersion    bool
    }
    

    解析配置文件的逻辑在lxc/config目录下,入口点是file.go文件中的LoadConfig函数。值得一提的是,lxc工具还支持命令的简写,通过源码我们可以发现只需要用lxc alias工具管理一个alias的键值映射即可。

    这里我们深入分析一下,lxc是如何将config作为一个全局变量进行传递的。以刚才提到的alias命令为例,在添加子命令的时候main.go中的有这样的代码。

        // alias sub-command
    	aliasCmd := cmdAlias{global: &globalCmd}
    	app.AddCommand(aliasCmd.Command())
    

    cmdAlias是在alias.go中的一个结构体,结构中包括一个名叫globalcmdGlobal对象,此外还有Command函数。添加子命令时程序先将globalCmd传递到cmdAlias中,再将Command函数注册到alias子命令的映射中,这样比较巧妙地将全局参数传递到了alias子命令中。

    当完成了参数解析后,具体的执行逻辑将会转移到各个子命令的Command函数中,我们这里挑选最常见的launch命令,完整地来看一次容器的创建流程。

    首先,我们假定运行的命令为lxc launch ubuntu:16.04 u1,看看这条命令在launch.go是如何处理的。 首先,我们发现cmdLaunch这个结构体与其他文件有一定的差异。

    type cmdLaunch struct {
    	global *cmdGlobal
    	init   *cmdInit
    }
    

    看见了吗,多了一个cmdInit对象。不难发现这个对象就是init.go中的对象,结合launch的具体过程我们可以想象,launch.go中可能是分两步执行launch过程,首先是lxc init ubuntu:16.04 u1,接下来lxc start u1

    
    func (c *cmdLaunch) Command() *cobra.Command {
    	cmd := c.init.Command()
    	cmd.Use = i18n.G("launch [<remote>:]<image> [<remote>:][<name>]")
    	cmd.Short = i18n.G("Create and start containers from images")
    	cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
    		`Create and start containers from images`))
    	cmd.Example = cli.FormatSection("", i18n.G(
    		`lxc launch ubuntu:16.04 u1
    
    lxc launch ubuntu:16.04 u1 < config.yaml
        Create and start the container with configuration from config.yaml`))
    	cmd.Hidden = false
    
    	cmd.RunE = c.Run
    
    	return cmd
    }
    
    

    源码中我们可以发现果然情况如此,launch.go的注册直接复用了init.go,只是将描述信息和运行函数进行了复写,这样可以将一些init的逻辑直接复用。我们定位到运行函数Run

    func (c *cmdLaunch) Run(cmd *cobra.Command, args []string) error {
    	conf := c.global.conf
    
    	// Sanity checks
    	exit, err := c.global.CheckArgs(cmd, args, 1, 2)
    	if exit {
    		return err
    	}
    
    	// Call the matching code from init
    	d, name, err := c.init.create(conf, args)
    	if err != nil {
    		return err
    	}
    
    	// Get the remote
    	var remote string
    	if len(args) == 2 {
    		remote, _, err = conf.ParseRemote(args[1])
    		if err != nil {
    			return err
    		}
    	} else {
    		remote, _, err = conf.ParseRemote("")
    		if err != nil {
    			return err
    		}
    	}
    
    	// Start the container
    	if !c.global.flagQuiet {
    		fmt.Printf(i18n.G("Starting %s")+"\n", name)
    	}
    
    	req := api.InstanceStatePut{
    		Action:  "start",
    		Timeout: -1,
    	}
    
    	op, err := d.UpdateInstanceState(name, req, "")
    	if err != nil {
    		return err
    	}
    
    	progress := utils.ProgressRenderer{
    		Quiet: c.global.flagQuiet,
    	}
    	_, err = op.AddHandler(progress.UpdateOp)
    	if err != nil {
    		progress.Done("")
    		return err
    	}
    
    	// Wait for operation to finish
    	err = utils.CancelableWait(op, &progress)
    	if err != nil {
    		progress.Done("")
    		prettyName := name
    		if remote != "" {
    			prettyName = fmt.Sprintf("%s:%s", remote, name)
    		}
    
    		return fmt.Errorf("%s\n"+i18n.G("Try `lxc info --show-log %s` for more info"), err, prettyName)
    	}
    
    	progress.Done("")
    	return nil
    }
    

    这段逻辑中除了输出显示的部分外,首先检查了一下参数数量和类型,接下来调用init逻辑中的create函数,接下来解析remote参数,然后构造了一个请求,针对该请求注册一个回调函数,最后执行同步性请求。那么这个部分中我们首先会关注init.gocreate函数。

    func (c *cmdInit) create(conf *config.Config, args []string) (lxd.InstanceServer, string, error) {
    	var name string
    	var image string
    	var remote string
    	var iremote string
    	var err error
    	var stdinData api.InstancePut
    	var devicesMap map[string]map[string]string
    	var configMap map[string]string
    
    	// If stdin isn't a terminal, read text from it
    	// ...
    
    	if len(args) > 0 {
    		// ... 指定的是remote:container的容器,解析出remote
    	}
    
    	if c.flagEmpty {
    		if len(args) > 1 {
    			return nil, "", fmt.Errorf(i18n.G("--empty cannot be combined with an image name"))
    		}
    
    		if len(args) == 0 {
    			remote, name, err = conf.ParseRemote("")
    			if err != nil {
    				return nil, "", err
    			}
    		} else if len(args) == 1 {
    			// Switch image / container names
    			name = image
    			remote = iremote
    			image = ""
    			iremote = ""
    		}
    	}
    
    	d, err := conf.GetInstanceServer(remote)
    	if err != nil {
    		return nil, "", err
    	}
    
    	if c.flagTarget != "" {
    		d = d.UseTarget(c.flagTarget)
    	}
    
    	profiles := []string{}
    	for _, p := range c.flagProfile {
    		profiles = append(profiles, p)
    	}
    
    	// 打印开始创建的信息
    
    	if len(stdinData.Devices) > 0 {
    		devicesMap = stdinData.Devices
    	} else {
    		devicesMap = map[string]map[string]string{}
    	}
    
    	if c.flagNetwork != "" {
    		network, _, err := d.GetNetwork(c.flagNetwork)
    		if err != nil {
    			return nil, "", err
    		}
    
    		if network.Type == "bridge" {
    			devicesMap[c.flagNetwork] = map[string]string{"type": "nic", "nictype": "bridged", "parent": c.flagNetwork}
    		} else {
    			devicesMap[c.flagNetwork] = map[string]string{"type": "nic", "nictype": "macvlan", "parent": c.flagNetwork}
    		}
    	}
    
    	if len(stdinData.Config) > 0 {
    		configMap = stdinData.Config
    	} else {
    		configMap = map[string]string{}
    	}
    	for _, entry := range c.flagConfig {
    		if !strings.Contains(entry, "=") {
    			return nil, "", fmt.Errorf(i18n.G("Bad key=value pair: %s"), entry)
    		}
    
    		fields := strings.SplitN(entry, "=", 2)
    		configMap[fields[0]] = fields[1]
    	}
    
    	// Check if the specified storage pool exists.
    	if c.flagStorage != "" {
    		_, _, err := d.GetStoragePool(c.flagStorage)
    		if err != nil {
    			return nil, "", err
    		}
    
    		devicesMap["root"] = map[string]string{
    			"type": "disk",
    			"path": "/",
    			"pool": c.flagStorage,
    		}
    	}
    
    	// Decide whether we are creating a container or a virtual machine.
    	instanceDBType := api.InstanceTypeContainer
    	if c.flagVM {
    		instanceDBType = api.InstanceTypeVM
    	}
    
    	// Setup instance creation request
    	req := api.InstancesPost{
    		Name:         name,
    		InstanceType: c.flagType,
    		Type:         instanceDBType,
    	}
    	req.Config = configMap
    	req.Devices = devicesMap
    
    	if !c.flagNoProfiles && len(profiles) == 0 {
    		if len(stdinData.Profiles) > 0 {
    			req.Profiles = stdinData.Profiles
    		} else {
    			req.Profiles = nil
    		}
    	} else {
    		req.Profiles = profiles
    	}
    	req.Ephemeral = c.flagEphemeral
    
    	var opInfo api.Operation
    	if !c.flagEmpty {
    		// Get the image server and image info
    		iremote, image = c.guessImage(conf, d, remote, iremote, image)
    		var imgRemote lxd.ImageServer
    		var imgInfo *api.Image
    
    		// Connect to the image server
    		if iremote == remote {
    			imgRemote = d
    		} else {
    			imgRemote, err = conf.GetImageServer(iremote)
    			if err != nil {
    				return nil, "", err
    			}
    		}
    
    		// Deal with the default image
    		if image == "" {
    			image = "default"
    		}
    
    		// Optimisation for simplestreams
    		if conf.Remotes[iremote].Protocol == "simplestreams" {
    			imgInfo = &api.Image{}
    			imgInfo.Fingerprint = image
    			imgInfo.Public = true
    			req.Source.Alias = image
    		} else {
    			// Attempt to resolve an image alias
    			alias, _, err := imgRemote.GetImageAlias(image)
    			if err == nil {
    				req.Source.Alias = image
    				image = alias.Target
    			}
    
    			// Get the image info
    			imgInfo, _, err = imgRemote.GetImage(image)
    			if err != nil {
    				return nil, "", err
    			}
    		}
    
    		// Create the instance
    		op, err := d.CreateInstanceFromImage(imgRemote, *imgInfo, req)
    		if err != nil {
    			return nil, "", err
    		}
    
    		// Watch the background operation
    		progress := utils.ProgressRenderer{
    			Format: i18n.G("Retrieving image: %s"),
    			Quiet:  c.global.flagQuiet,
    		}
    
    		_, err = op.AddHandler(progress.UpdateOp)
    		if err != nil {
    			progress.Done("")
    			return nil, "", err
    		}
    
    		err = utils.CancelableWait(op, &progress)
    		if err != nil {
    			progress.Done("")
    			return nil, "", err
    		}
    		progress.Done("")
    
    		// Extract the container name
    		info, err := op.GetTarget()
    		if err != nil {
    			return nil, "", err
    		}
    
    		opInfo = *info
    	} else {
    		req.Source.Type = "none"
    
    		op, err := d.CreateInstance(req)
    		if err != nil {
    			return nil, "", err
    		}
    
    		err = op.Wait()
    		if err != nil {
    			return nil, "", err
    		}
    
    		opInfo = op.Get()
    	}
    
    	instances, ok := opInfo.Resources["instances"]
    	if !ok || len(instances) == 0 {
    		// Try using the older "containers" field
    		instances, ok = opInfo.Resources["containers"]
    		if !ok || len(instances) == 0 {
    			return nil, "", fmt.Errorf(i18n.G("Didn't get any affected image, instance or snapshot from server"))
    		}
    	}
    
    	if len(instances) == 1 && name == "" {
    		fields := strings.Split(instances[0], "/")
    		name = fields[len(fields)-1]
    		fmt.Printf(i18n.G("Instance name is: %s")+"\n", name)
    	}
    
    	// Validate the network setup
    	c.checkNetwork(d, name)
    
    	return d, name, nil
    }
    
    

    可以看到,这部分虽然代码很长,但是大部分逻辑都是在构造容器的config,如果用户没有指定参数的话使用什么默认参数,例如镜像、网络、存储池、profile等等。核心代码是使用GetInstanceServer命令得到了一个InstanceServer对象,构造参数时也会通过这个对象查询,最后使用该对象的CreateInstanceFromImage(无镜像时用CreateInstance生成容器。那么,这里我们看看remote.go中的GetInstanceServer

    // GetInstanceServer returns a InstanceServer struct for the remote
    func (c *Config) GetInstanceServer(name string) (lxd.InstanceServer, error) {
    	// Handle "local" on non-Linux
    	if name == "local" && runtime.GOOS != "linux" {
    		return nil, ErrNotLinux
    	}
    
    	// Get the remote
    	remote, ok := c.Remotes[name]
    	if !ok {
    		return nil, fmt.Errorf("The remote \"%s\" doesn't exist", name)
    	}
    
    	// Sanity checks
    	if remote.Public || remote.Protocol == "simplestreams" {
    		return nil, fmt.Errorf("The remote isn't a private LXD server")
    	}
    
    	// Get connection arguments
    	args, err := c.getConnectionArgs(name)
    	if err != nil {
    		return nil, err
    	}
    
    	// Unix socket
    	if strings.HasPrefix(remote.Addr, "unix:") {
    		d, err := lxd.ConnectLXDUnix(strings.TrimPrefix(strings.TrimPrefix(remote.Addr, "unix:"), "//"), args)
    		if err != nil {
    			return nil, err
    		}
    
    		if remote.Project != "" && remote.Project != "default" {
    			d = d.UseProject(remote.Project)
    		}
    
    		if c.ProjectOverride != "" {
    			d = d.UseProject(c.ProjectOverride)
    		}
    
    		return d, nil
    	}
    
    	// HTTPs
    	if remote.AuthType != "candid" && (args.TLSClientCert == "" || args.TLSClientKey == "") {
    		return nil, fmt.Errorf("Missing TLS client certificate and key")
    	}
    
    	d, err := lxd.ConnectLXD(remote.Addr, args)
    	if err != nil {
    		return nil, err
    	}
    
    	if remote.Project != "" && remote.Project != "default" {
    		d = d.UseProject(remote.Project)
    	}
    
    	if c.ProjectOverride != "" {
    		d = d.UseProject(c.ProjectOverride)
    	}
    
    	return d, nil
    }
    

    针对两种连接模式,该函数使用lxd包中的ConnectLXDUnixConnectLXD两个函数连接。值得注意的是,这里的lxd并不是指的lxd这个目录,仔细看会发现这里的lxd包实际上在client这个目录下,而lxd目录下的包名实际上叫main。个人认为lxd在命名方面确实存在着很多的混淆点,除去lxd的命令行工具叫做lxc很可能与lxc 1.0让人产生误解,这里的包名稍不注意也会弄错。话说回来,这样我们知道了client这个目录下的代码应当是用来生成客户端向服务端发起的请求的。

    client/connect.go中,我们找到了ConnectLXD函数。

    // ConnectLXD lets you connect to a remote LXD daemon over HTTPs.
    //
    // A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
    //
    // If connecting to a LXD daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
    //
    // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
    func ConnectLXD(url string, args *ConnectionArgs) (InstanceServer, error) {
    	logger.Debugf("Connecting to a remote LXD over HTTPs")
    
    	// Cleanup URL
    	url = strings.TrimSuffix(url, "/")
    
    	return httpsLXD(url, args)
    }
    
    // Internal function called by ConnectLXD and ConnectPublicLXD
    func httpsLXD(url string, args *ConnectionArgs) (InstanceServer, error) {
    	// Use empty args if not specified
    	if args == nil {
    		args = &ConnectionArgs{}
    	}
    
    	// Initialize the client struct
    	server := ProtocolLXD{
    		httpCertificate:  args.TLSServerCert,
    		httpHost:         url,
    		httpProtocol:     "https",
    		httpUserAgent:    args.UserAgent,
    		bakeryInteractor: args.AuthInteractor,
    		chConnected:      make(chan struct{}, 1),
    	}
    
    	if args.AuthType == "candid" {
    		server.RequireAuthenticated(true)
    	}
    
    	// Setup the HTTP client
    	httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy)
    	if err != nil {
    		return nil, err
    	}
    
    	if args.CookieJar != nil {
    		httpClient.Jar = args.CookieJar
    	}
    
    	server.http = httpClient
    	if args.AuthType == "candid" {
    		server.setupBakeryClient()
    	}
    
    	// Test the connection and seed the server information
    	if !args.SkipGetServer {
    		_, _, err := server.GetServer()
    		if err != nil {
    			return nil, err
    		}
    	}
    	return &server, nil
    }
    
    

    而这里已经接近https请求发送的底层了,我们不再深入分析了,只是需要注意的是lxd使用的是位于util.go中几乎自己实现的tlsHttpClient,而不像我想象的那样使用了第三方的https请求库。至于unix socket部分和https请求类似。

    那么,在获取了这个InstanceServer后,程序使用了对象的CreateInstanceFromImage函数来创建容器。找到interfaces.go中的InstanceServer后我们发现这是一个接口,由lxd.go中的ProtocolLXD实现。我们来看看这个CreateInstanceFromImage函数(该函数实现在lxd_instances.go中)。

    
    // CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
    func (r *ProtocolLXD) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
    	// Set the minimal source fields
    	req.Source.Type = "image"
    
    	// Optimization for the local image case
    	if r == source {
    		// Always use fingerprints for local case
    		req.Source.Fingerprint = image.Fingerprint
    		req.Source.Alias = ""
    
    		op, err := r.CreateInstance(req)
    		if err != nil {
    			return nil, err
    		}
    
    		rop := remoteOperation{
    			targetOp: op,
    			chDone:   make(chan bool),
    		}
    
    		// Forward targetOp to remote op
    		go func() {
    			rop.err = rop.targetOp.Wait()
    			close(rop.chDone)
    		}()
    
    		return &rop, nil
    	}
    
    	// Minimal source fields for remote image
    	req.Source.Mode = "pull"
    
    	// If we have an alias and the image is public, use that
    	if req.Source.Alias != "" && image.Public {
    		req.Source.Fingerprint = ""
    	} else {
    		req.Source.Fingerprint = image.Fingerprint
    		req.Source.Alias = ""
    	}
    
    	// Get source server connection information
    	info, err := source.GetConnectionInfo()
    	if err != nil {
    		return nil, err
    	}
    
    	req.Source.Protocol = info.Protocol
    	req.Source.Certificate = info.Certificate
    
    	// Generate secret token if needed
    	if !image.Public {
    		secret, err := source.GetImageSecret(image.Fingerprint)
    		if err != nil {
    			return nil, err
    		}
    
    		req.Source.Secret = secret
    	}
    
    	return r.tryCreateInstance(req, info.Addresses)
    }
    

    这里简单的对镜像进行获取后,调用tryCreateInstance

    
    func (r *ProtocolLXD) tryCreateInstance(req api.InstancesPost, urls []string) (RemoteOperation, error) {
    	if len(urls) == 0 {
    		return nil, fmt.Errorf("The source server isn't listening on the network")
    	}
    
    	rop := remoteOperation{
    		chDone: make(chan bool),
    	}
    
    	operation := req.Source.Operation
    
    	// Forward targetOp to remote op
    	go func() {
    		success := false
    		errors := map[string]error{}
    		for _, serverURL := range urls {
    			if operation == "" {
    				req.Source.Server = serverURL
    			} else {
    				req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
    			}
    
    			op, err := r.CreateInstance(req)
    			if err != nil {
    				errors[serverURL] = err
    				continue
    			}
    
    			rop.targetOp = op
    
    			for _, handler := range rop.handlers {
    				rop.targetOp.AddHandler(handler)
    			}
    
    			err = rop.targetOp.Wait()
    			if err != nil {
    				errors[serverURL] = err
    				continue
    			}
    
    			success = true
    			break
    		}
    
    		if !success {
    			rop.err = remoteOperationError("Failed instance creation", errors)
    		}
    
    		close(rop.chDone)
    	}()
    
    	return &rop, nil
    }
    

    这里面涉及到了golang的并发实现go func() {}(),以建立一个异步的请求,并注册回调函数到rop中,在请求回复后执行。

  • DPDK Pktgen和Testpmd验证试验

    Ref: Version: DPDK 19.08 / Pktgen 3.7.2

    +--------+---------------+               +-------------------+---------------+
    |        | socket file 1 |   <------->   | vhost-user port 1 |               |
    |        +---------------+               +-------------------+     Docker    |
    | host   |     pktgen    |               |      testpmd      |   container   |
    |        +---------------+               +-------------------+               |
    |        | socket file 0 |   <------->   | vhost-user port 0 |               |
    +--------+---------------+               +-------------------+---------------+
    

    Compile DPDK and Pktgen

    DPDK

    export RTE_SDK=~/dpdk/dpdk-19.08
    export RTE_TARGET=x86_64-native-linuxapp-gcc
    sed -ri  's,(CONFIG_RTE_LIBRTE_VHOST).*,\1y' config/common_base
    make config T=$RTE_TARGET
    sed -ri 's,(PMD_PCAP).*,\1y' build/.config
    make
    

    Pktgen

    export RTE_SDK=~/dpdk/dpdk-19.08
    export RTE_TARGET=build
    make
    

    Build a Docker image

    Create a dockerfile in the directory contains DPDK_SDK.

    FROM ubuntu:16.04
    WORKDIR /root/dpdk
    COPY dpdk-19.08 /root/dpdk/.
    ENV PATH "$PATH:/root/dpdk/$RTE_TARGET/app/"
    RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \
        apt update && apt install -y libnuma-dev libpcap-dev
    ENTRYPOINT ["/bin/bash"]
    

    Allocate HugePage

    Modify /etc/default/grub.

    GRUB_CMDLINE_LINUX_DEFAULT="default_hugepagesz=1GB hugepagesz=1G hugepages=8"
    

    Update the grub file and reboot to take effect.

    sudo update-grub
    reboot
    mkdir -p /dev/hugepages
    sudo mount -t hugetlbfs none /dev/hugepages
    

    Run the Testpmd container

    sudo docker run -ti --rm --name=test \
    -v /dev/hugepages:/dev/hugepages \
    -v /tmp/virtio/:/tmp/virtio/ \
    --privileged dpdk
    

    Type the commands below inside the container shell

    testpmd -l 0-1 -n 1 --socket-mem 1024,1024 \
    --vdev 'eth_vhost0,iface=/tmp/virtio/sock0' --vdev 'eth_vhost1,iface=/tmp/virtio/sock1' \
    --file-prefix=test --no-pci \
    -- -i --forward-mode=io --auto-start
    

    Some usefule runtime functions

    show port stats all
    

    Generate the packets

    sudo pktgen -l 2,3,4 -n 2 --vdev=virtio_user0,path=/tmp/virtio/sock0 --vdev=virtio_user1,path=/tmp/virtio/sock1 -- -P -m "3.0,4.1"
    

    Some useful runtime functions

    set all rate 10 # set the sending rate at 10%
    set 0 count 100 # request the channel 0 to send 100 packets in total
    str # start