标签: LXD

  • LXD中Windows VM的初始化方法

    基本思路:使用Cloudbase-init,在容器初始化的时候将需要配置的IP地址配置信息填入到LXD容器的config里,借助LXD的配置功能为每一个VM配置一个名字为tzdata的光盘镜像,cloudbase-init在windows vm启动时自动读取镜像,将需要的配置执行。

    华为云对cloudbase-init工具的介绍

    Cloudbase-init基本原理类似于cloud-init,只是由于cloud-init并没有提供对windows的支持才有了cloudbase-init,这个工具可以在虚拟机首次启动时根据某一些信息来初始化VM,使得VM在云基础设施上配置时可以完全自动化。这些信息来源是通过Service来确定的,例如各个云平台的自定义初始化方法(AWS、GCP),或者一些通用的虚拟机话基础设施(OpenStack、MaaS、VMware),具体的Service在文档中有详细描述(https://cloudbase-init.readthedocs.io/en/latest/services.html#nocloud-configuration-drive)。每一个Service又支持多个Plugin来配置不同的设置。

    由于LXD本身对于虚拟机的支持还是experimental,大部分的qemu配置都很难直接应用在LXD config里(有一个raw.qemu但也只是单纯将其参数补充在qemu运行命令之后),对于Windows的支持就更少了。好在LXD支持添加一个名为cloud-init:config的device,结合填入的user.meta-data等等参数即可以自动完成cloud-init初始化光盘的创建和连接。为了让cloud-init初始化光盘的配置能应用在cloudbase-init上,我们需要选择cloudbaseinit.metadata.services.nocloudservice.NoCloudConfigDriveService 这个配置和cloud-init的配置基本上是一致的,但是需要注意的是对于网络配置而言只能选择cloud-init的V1版本,对于V2版本(即官方宣称的network-config配置文件)是不能使用的(非常确定,因为我已经到源码里找了V2实现是空的)。

    V1版本的配置方法也非常讲究,通过分析源码我发现他针对每一个网络端口并不是根据名称来配置的,而是根据MAC地址,因此如果需要修改的名字也只需要在配置项中指定即可。

    network-interfaces:|
      iface Ethernet0 inet static
      address 10.0.0.2
      network 10.0.0.0
      netmask 255.255.255.0
      broadcast 10.0.0.255
      gateway 10.0.0.1
      hwaddress ether 00:11:22:33:44:55
    

    还有需要注意的一点是,如果是使用lxc工具使用edit修改的VM config,那么yaml需要从那一项开始对齐。例如

    Config:
      user.meta-data: |
        network-interfaces: |
          iface Ethernet0 inet static
    .…
    

    如果顶头写的话,那么lxc在解析修改后的yml文件时认为是无效参数直接忽略掉,而且不会有任何报错提示。因此,在修改lxc config后一定要再show一次检查一下是否修改成功。

    完成配置后,可以检查虚拟机里的配置是否正常了,但是这里又出现了一个问题是,cloudbase-init在完成了一次修改后会在注册表里写入一些信息,这样第二次启动的时候就会检查有无相关信息,如果有那么会直接跳过配置项。这种策略在我们调试cloudbase-init配置的时候很麻烦。解决方法是,手动在具有管理员权限的powershell里执行(https://ask.cloudbase.it/question/1334/sethostnameplugin-execution-already-done/)

    Remove-Item -Recurse "HKLM:\\Software\Cloudbase Solutions"
    
  • LXD使用ZFS做存储后端的扩容方法

    以使用lxd init初始化创建的default ZFS存储池为例,如果是apt安装版本则该存储文件在/var/lib/lxd/disks/default.img,以snap方式安装的则在/var/snap/lxd/common/lxd/disks/default.img,对该文件进行扩容处理。

    在操作之前首先保证自己有root权限,安装有zfsutils-linux,并保证所有的LXD容器均处于stop状态

    这里以增加20G容量为例。

    truncate -s +20G /var/lib/lxd/disks/default.img
    zpool set autoexpand=on default
    device=$(zpool status -vg default)
    zpool online -e default $device
    zpool set autoexpand=off default
    service lxd restart
    

    接下来就可以用lxc storage info default命令查看default存储池的扩容情况了。

  • 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中,在请求回复后执行。