自己动手写Docker系列 -- 6.1 ip分配管理

213 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

简介

在前面的文章中,我们完成了基本的容器操作,容器已经能在无网络情况下运行,接下来的文章中,将开始网络相关的部分,本地是编写一个ip分配工具

源码说明

同时放到了Gitee和Github上,都可进行获取

本章节对应的版本标签是:5.2,防止后面代码过多,不好查看,可切换到标签版本进行查看

代码实现

在本篇中将实现一个ip分配管理工具

主要的思路如下:

1 给定一个网段,能自动分配未使用的IP地址

2 能回收IP地址,提供给下次使用

IP地址需要一个存储,目前如书中所说,简单采用字符串进行记录,一个位置就是一个IP标识位

IP地址分配管理

下面的代码包含了IP地址的分配,回收和再分配

const ipamDefaultAllocatorPath = "/var/run/mydocker/network/ipam/subnet.json"

// IPAM 存放IP地址分配信息
type IPAM struct {
	// 分配文件存放位置
	SubnetAllocatorPath string
	// 网段和位图算法的数组map,key是网段,value是分配的位图数组
	Subnets *map[string]string
}

// 初始化一个IPAMd对象
var ipAllocator = &IPAM{
	SubnetAllocatorPath: ipamDefaultAllocatorPath,
}

// 加载网段地址分配信息
func (ipam *IPAM) load() error {
	if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
	defer subnetConfigFile.Close()
	if err != nil {
		return err
	}
	subnetJson := make([]byte, 2000)
	n, err := subnetConfigFile.Read(subnetJson)
	if err != nil {
		return err
	}

	err = json.Unmarshal(subnetJson[:n], ipam.Subnets)
	if err != nil {
		return fmt.Errorf("dump allocation info err: %v", err)
	}
	log.Infof("load ipam file from: %s", subnetConfigFile.Name())
	return nil
}

// 存储网段地址分配信息
func (ipam *IPAM) dump() error {
	ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)
	if _, err := os.Stat(ipamConfigFileDir); err != nil {
		if os.IsNotExist(err) {
			os.MkdirAll(ipamConfigFileDir, 0644)
		} else {
			return err
		}
	}

	subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
	defer subnetConfigFile.Close()
	if err != nil {
		return err
	}

	ipamConfigJson, err := json.Marshal(ipam.Subnets)
	if err != nil {
		return err
	}
	_, err = subnetConfigFile.Write(ipamConfigJson)
	if err != nil {
		return err
	}
	log.Infof("dump ipam file from: %s", ipamConfigFileDir)
	return nil
}

// Allocate 在网段中分配一个可用的IP地址
func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
	// 存储网段中地址分配信息的数组
	ipam.Subnets = &map[string]string{}

	// 从文件中加载已经分配了的网段信息
	err = ipam.load()
	if err != nil {
		return nil, fmt.Errorf("load subnet file err: %v", err)
	}

	// net.ipnet.nask.size() 返回网段的子网掩码的总长度和网段前面的固定位的长度
	// 比如 127.0.0.0/8 网段的子网掩码是 255.0.0.0
	// 返回的是前面255所对应的位数和总位数,即8和24
	one, size := subnet.Mask.Size()

	// 如果之前没有分配过这个网段,则初始化网段的分配配置
	if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {
		// 用0填满这个网段的配置, 1<<uint8(size-one)表示这个网段中有多少个可用的地址
		// size - one 是子网掩码后面的网络位数,2^(size-one)(即1<<uint8(size-one))表示可用的IP数
		(*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1<<uint8(size-one))
	}

	// 遍历网段的位图数组
	for c := range (*ipam.Subnets)[subnet.String()] {
		// 找到网段中为0的项和数组序号,即可分配的IP
		if (*ipam.Subnets)[subnet.String()][c] == '0' {
			// 设置当前的序号值为1,即分配这个IP
			ipalloc := []byte((*ipam.Subnets)[subnet.String()])

			// Go中字符串不能修改,通过转成byte数组,再转成字符串赋值
			ipalloc[c] = '1'
			(*ipam.Subnets)[subnet.String()] = string(ipalloc)

			// 这里的IP为初始IP,比如192.168.0.0/16,这里就是192.168.0.0
			ip = subnet.IP

			// 通过网段的IP与上面的偏移相加计算出分配的IP地址,由于IP地址是uint的一个数组
			// 需要通过数组中的每一项加所需要的值,比如网段172.16.0.0/12,数组序号是65555
			// 那么在[172,16,0,0]上依次加[uint8(65555 >> 24)、[uint8(65555 >> 16)、[uint8(65555 >> 8)、[uint8(65555 >> 8)
			// 即[0, 1, 0, 19],那么最后得到的172.17.0.19
			for t := uint(4); t > 0; t -= 1 {
				[]byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))
			}

			// 由于IP是从1开始的,所以最后加1
			ip[3] += 1
			break
		}
	}

	// 通过dump将分配结果保存到文件中
	return ip, ipam.dump()
}

// Release 地址释放
func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
	ipam.Subnets = &map[string]string{}

	// 从文件中加载网段分配信息
	err := ipam.load()
	if err != nil {
		return fmt.Errorf("load subnet file err: %v", err)
	}

	// 计算IP地址在网段位图数组中的索引位置
	index := 0
	// 将IP地址转换成4个字节的表现形式
	releaseIp := ipaddr.To4()
	// 由于IP是从1开始分配的,所以转换成索引应减一
	releaseIp[3] -= 1
	// 与分配IP相反,释放IP获得索引的方式是将IP地址的每一位相减后分别左移将对应的数值加到索引上
	for t := uint(4); t > 0; t -= 1 {
		index += int(releaseIp[t-1]-subnet.IP[t-1]) << ((4 - t) * 8)
	}
	log.Infof("release index: %d", index)

	// 将分配的位图索引中的位置的值置为0
	ipalloc := []byte((*ipam.Subnets)[subnet.String()])
	ipalloc[index] = '0'
	(*ipam.Subnets)[subnet.String()] = string(ipalloc)

	// 保存释放IP后的配置信息
	return ipam.dump()
}

测试

写一个测试验证下

func TestIPAM_Allocate(t *testing.T) {
	// 首先把文件删除了,清理重置下环境
	_ = os.RemoveAll(ipamDefaultAllocatorPath)

	// 每次释放和分配ip时,都需要重新调用下面的函数进行IPNet的获取,因为函数调用后,IPNet的值会发生变化
	_, ipNet, _ := net.ParseCIDR("192.168.0.0/24")
	// 第一次分配
	ip1, err := ipAllocator.Allocate(ipNet)
	assert.Equal(t, nil, err)
	assert.Equal(t, "192.168.0.1", ip1.String())

	// 第二个ip分配
	_, ipNet, _ = net.ParseCIDR("192.168.0.0/24")
	ip2, err := ipAllocator.Allocate(ipNet)
	assert.Equal(t, nil, err)
	assert.Equal(t, "192.168.0.2", ip2.String())

	// 释放调第一个IP
	_, ipNet, _ = net.ParseCIDR("192.168.0.0/24")
	assert.Equal(t, nil, ipAllocator.Release(ipNet, &ip1))

	// 能分配得第一个IP
	_, ipNet, _ = net.ParseCIDR("192.168.0.0/24")
	ip3, err := ipAllocator.Allocate(ipNet)
	assert.Equal(t, nil, err)
	assert.Equal(t, "192.168.0.1", ip3.String())

	// 分配第三个IP
	_, ipNet, _ = net.ParseCIDR("192.168.0.0/24")
	ip4, err := ipAllocator.Allocate(ipNet)
	assert.Equal(t, nil, err)
	assert.Equal(t, "192.168.0.3", ip4.String())
}