Merge pull request #12 from squeed/host-local-multi

ipam/host-local: support multiple IP ranges
This commit is contained in:
Dan Williams 2017-06-14 21:55:30 -05:00 committed by GitHub
commit e2558a03bb
13 changed files with 1454 additions and 484 deletions

View File

@ -31,6 +31,16 @@ func PrevIP(ip net.IP) net.IP {
return intToIP(i.Sub(i, big.NewInt(1)))
}
// Cmp compares two IPs, returning the usual ordering:
// a < b : -1
// a == b : 0
// a > b : 1
func Cmp(a, b net.IP) int {
aa := ipToInt(a)
bb := ipToInt(b)
return aa.Cmp(bb)
}
func ipToInt(ip net.IP) *big.Int {
if v := ip.To4(); v != nil {
return big.NewInt(0).SetBytes(v)

View File

@ -5,30 +5,41 @@ it can include a DNS configuration from a `resolv.conf` file on the host.
## Overview
host-local IPAM plugin allocates IPv4 addresses out of a specified address range.
host-local IPAM plugin allocates ip addresses out of a set of address ranges.
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
## Example configurations
IPv4:
```json
{
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16",
"rangeStart": "10.10.1.20",
"rangeEnd": "10.10.3.50",
"gateway": "10.10.0.254",
"ranges": [
{
"subnet": "10.10.0.0/16",
"rangeStart": "10.10.1.20",
"rangeEnd": "10.10.3.50",
"gateway": "10.10.0.254"
},
{
"subnet": "3ffe:ffff:0:01ff::/64",
"rangeStart": "3ffe:ffff:0:01ff::0010",
"rangeEnd": "3ffe:ffff:0:01ff::0020"
}
],
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" }
{ "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
{ "dst": "3ffe:ffff:0:01ff::1/64" }
],
"dataDir": "/var/my-orchestrator/container-ipam-state"
"dataDir": "/run/my-orchestrator/container-ipam-state"
}
}
```
IPv6:
Previous versions of the `host-local` allocator did not support the `ranges`
property, and instead expected a single range on the top level. This is
deprecated but still supported.
```json
{
"ipam": {
@ -47,36 +58,65 @@ IPv6:
We can test it out on the command-line:
```bash
$ export CNI_COMMAND=ADD
$ export CNI_CONTAINERID=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
$ echo '{ "name": "default", "ipam": { "type": "host-local", "subnet": "203.0.113.0/24" } }' | ./host-local
$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ {"subnet": "203.0.113.0/24"}, {"subnet": "2001:db8:1::/64"}], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
```
```json
{
"ip4": {
"ip": "203.0.113.1/24"
}
"ips": [
{
"version": "4",
"address": "203.0.113.2/24",
"gateway": "203.0.113.1"
},
{
"version": "6",
"address": "2001:db8:1::2/64",
"gateway": "2001:db8:1::1"
}
],
"dns": {}
}
```
## Network configuration reference
* `type` (string, required): "host-local".
* `subnet` (string, required): CIDR block to allocate out of.
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block.
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
* `ranges`, (array, required, nonempty) an array of range objects:
* `subnet` (string, required): CIDR block to allocate out of.
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6
* `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
Older versions of the `host-local` plugin did not support the `ranges` array. Instead,
all the properties in the `range` object were top-level. This is still supported but deprecated.
## Supported arguments
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
* `ip`: request a specific IP address from the subnet. If it's not available, the plugin will exit with an error
* `ip`: request a specific IP address from a subnet.
The following [args conventions](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) are supported:
* `ips` (array of strings): A list of custom IPs to attempt to allocate
### Custom IP allocation
For every requested custom IP, the `host-local` allocator will request that IP
if it falls within one of the `range` objects. Thus it is possible to specify
multiple custom IPs and multiple ranges.
If any requested IPs cannot be reserved, either because they are already in use
or are not part of a specified range, the plugin will return an error.
## Files
Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`. The prefix can be customized with the `dataDir` option listed above.
Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`.
The path can be customized with the `dataDir` option listed above. Environments
where IPs are released automatically on reboot (e.g. running containers are not
restored) may wish to specify `/var/run/cni` or another tmpfs mounted directory
instead.

View File

@ -15,196 +15,115 @@
package allocator
import (
"encoding/base64"
"fmt"
"log"
"net"
"os"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ip"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
)
type IPAllocator struct {
// start is inclusive and may be allocated
netName string
ipRange Range
store backend.Store
rangeID string // Used for tracking last reserved ip
}
type RangeIter struct {
low net.IP
high net.IP
cur net.IP
start net.IP
// end is inclusive and may be allocated
end net.IP
conf *IPAMConfig
store backend.Store
}
func NewIPAllocator(conf *IPAMConfig, store backend.Store) (*IPAllocator, error) {
// Can't create an allocator for a network with no addresses, eg
// a /32 or /31
ones, masklen := conf.Subnet.Mask.Size()
if ones > masklen-2 {
return nil, fmt.Errorf("Network %v too small to allocate from", conf.Subnet)
}
func NewIPAllocator(netName string, r Range, store backend.Store) *IPAllocator {
// The range name (last allocated ip suffix) is just the base64
// encoding of the bytes of the first IP
rangeID := base64.URLEncoding.EncodeToString(r.RangeStart)
var (
start net.IP
end net.IP
err error
)
start, end, err = networkRange((*net.IPNet)(&conf.Subnet))
if err != nil {
return nil, err
return &IPAllocator{
netName: netName,
ipRange: r,
store: store,
rangeID: rangeID,
}
// skip the .0 address
start = ip.NextIP(start)
if conf.RangeStart != nil {
if err := validateRangeIP(conf.RangeStart, (*net.IPNet)(&conf.Subnet), start, end); err != nil {
return nil, err
}
start = conf.RangeStart
}
if conf.RangeEnd != nil {
if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet), start, end); err != nil {
return nil, err
}
end = conf.RangeEnd
}
return &IPAllocator{start, end, conf, store}, nil
}
func canonicalizeIP(ip net.IP) (net.IP, error) {
if ip.To4() != nil {
return ip.To4(), nil
} else if ip.To16() != nil {
return ip.To16(), nil
}
return nil, fmt.Errorf("IP %s not v4 nor v6", ip)
}
// Ensures @ip is within @ipnet, and (if given) inclusive of @start and @end
func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) error {
var err error
// Make sure we can compare IPv4 addresses directly
ip, err = canonicalizeIP(ip)
if err != nil {
return err
}
if !ipnet.Contains(ip) {
return fmt.Errorf("%s not in network: %s", ip, ipnet)
}
if start != nil {
start, err = canonicalizeIP(start)
if err != nil {
return err
}
if len(ip) != len(start) {
return fmt.Errorf("%s %d not same size IP address as start %s %d", ip, len(ip), start, len(start))
}
for i := 0; i < len(ip); i++ {
if ip[i] > start[i] {
break
} else if ip[i] < start[i] {
return fmt.Errorf("%s outside of network %s with start %s", ip, ipnet, start)
}
}
}
if end != nil {
end, err = canonicalizeIP(end)
if err != nil {
return err
}
if len(ip) != len(end) {
return fmt.Errorf("%s %d not same size IP address as end %s %d", ip, len(ip), end, len(end))
}
for i := 0; i < len(ip); i++ {
if ip[i] < end[i] {
break
} else if ip[i] > end[i] {
return fmt.Errorf("%s outside of network %s with end %s", ip, ipnet, end)
}
}
}
return nil
}
// Returns newly allocated IP along with its config
func (a *IPAllocator) Get(id string) (*current.IPConfig, []*types.Route, error) {
// Get alocates an IP
func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, error) {
a.store.Lock()
defer a.store.Unlock()
gw := a.conf.Gateway
if gw == nil {
gw = ip.NextIP(a.conf.Subnet.IP)
}
gw := a.ipRange.Gateway
var requestedIP net.IP
if a.conf.Args != nil {
requestedIP = a.conf.Args.IP
}
var reservedIP net.IP
if requestedIP != nil {
if gw != nil && gw.Equal(a.conf.Args.IP) {
return nil, nil, fmt.Errorf("requested IP must differ gateway IP")
if gw != nil && gw.Equal(requestedIP) {
return nil, fmt.Errorf("requested IP must differ from gateway IP")
}
subnet := net.IPNet{
IP: a.conf.Subnet.IP,
Mask: a.conf.Subnet.Mask,
if err := a.ipRange.IPInRange(requestedIP); err != nil {
return nil, err
}
err := validateRangeIP(requestedIP, &subnet, a.start, a.end)
reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
if err != nil {
return nil, nil, err
return nil, err
}
if !reserved {
return nil, fmt.Errorf("requested IP address %q is not available in network: %s %s", requestedIP, a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
}
reservedIP = requestedIP
reserved, err := a.store.Reserve(id, requestedIP)
} else {
iter, err := a.GetIter()
if err != nil {
return nil, nil, err
return nil, err
}
if reserved {
ipConfig := &current.IPConfig{
Version: "4",
Address: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
Gateway: gw,
for {
cur := iter.Next()
if cur == nil {
break
}
// don't allocate gateway IP
if gw != nil && cur.Equal(gw) {
continue
}
reserved, err := a.store.Reserve(id, cur, a.rangeID)
if err != nil {
return nil, err
}
if reserved {
reservedIP = cur
break
}
routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
}
return nil, nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
}
startIP, endIP := a.getSearchRange()
for cur := startIP; ; cur = a.nextIP(cur) {
// don't allocate gateway IP
if gw != nil && cur.Equal(gw) {
continue
}
reserved, err := a.store.Reserve(id, cur)
if err != nil {
return nil, nil, err
}
if reserved {
ipConfig := &current.IPConfig{
Version: "4",
Address: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
Gateway: gw,
}
routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
}
// break here to complete the loop
if cur.Equal(endIP) {
break
}
if reservedIP == nil {
return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
}
return nil, nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
version := "4"
if reservedIP.To4() == nil {
version = "6"
}
return &current.IPConfig{
Version: version,
Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask},
Gateway: gw,
}, nil
}
// Releases all IPs allocated for the container with given ID
// Release clears all IPs allocated for the container with given ID
func (a *IPAllocator) Release(id string) error {
a.store.Lock()
defer a.store.Unlock()
@ -212,66 +131,59 @@ func (a *IPAllocator) Release(id string) error {
return a.store.ReleaseByID(id)
}
// Return the start and end IP addresses of a given subnet, excluding
// the broadcast address (eg, 192.168.1.255)
func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) {
if ipnet.IP == nil {
return nil, nil, fmt.Errorf("missing field %q in IPAM configuration", "subnet")
}
ip, err := canonicalizeIP(ipnet.IP)
if err != nil {
return nil, nil, fmt.Errorf("IP not v4 nor v6")
// GetIter encapsulates the strategy for this allocator.
// We use a round-robin strategy, attempting to evenly use the whole subnet.
// More specifically, a crash-looping container will not see the same IP until
// the entire range has been run through.
// We may wish to consider avoiding recently-released IPs in the future.
func (a *IPAllocator) GetIter() (*RangeIter, error) {
i := RangeIter{
low: a.ipRange.RangeStart,
high: a.ipRange.RangeEnd,
}
if len(ip) != len(ipnet.Mask) {
return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch")
}
var end net.IP
for i := 0; i < len(ip); i++ {
end = append(end, ip[i]|^ipnet.Mask[i])
}
// Exclude the broadcast address for IPv4
if ip.To4() != nil {
end[3]--
}
return ipnet.IP, end, nil
}
// nextIP returns the next ip of curIP within ipallocator's subnet
func (a *IPAllocator) nextIP(curIP net.IP) net.IP {
if curIP.Equal(a.end) {
return a.start
}
return ip.NextIP(curIP)
}
// getSearchRange returns the start and end ip based on the last reserved ip
func (a *IPAllocator) getSearchRange() (net.IP, net.IP) {
var startIP net.IP
var endIP net.IP
// Round-robin by trying to allocate from the last reserved IP + 1
startFromLastReservedIP := false
lastReservedIP, err := a.store.LastReservedIP()
// We might get a last reserved IP that is wrong if the range indexes changed.
// This is not critical, we just lose round-robin this one time.
lastReservedIP, err := a.store.LastReservedIP(a.rangeID)
if err != nil && !os.IsNotExist(err) {
log.Printf("Error retriving last reserved ip: %v", err)
log.Printf("Error retrieving last reserved ip: %v", err)
} else if lastReservedIP != nil {
subnet := net.IPNet{
IP: a.conf.Subnet.IP,
Mask: a.conf.Subnet.Mask,
}
err := validateRangeIP(lastReservedIP, &subnet, a.start, a.end)
if err == nil {
startFromLastReservedIP = true
}
startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
}
if startFromLastReservedIP {
startIP = a.nextIP(lastReservedIP)
endIP = lastReservedIP
if i.high.Equal(lastReservedIP) {
i.start = i.low
} else {
i.start = ip.NextIP(lastReservedIP)
}
} else {
startIP = a.start
endIP = a.end
i.start = a.ipRange.RangeStart
}
return startIP, endIP
return &i, nil
}
// Next returns the next IP in the iterator, or nil if end is reached
func (i *RangeIter) Next() net.IP {
// If we're at the beginning, time to start
if i.cur == nil {
i.cur = i.start
return i.cur
}
// we returned .high last time, since we're inclusive
if i.cur.Equal(i.high) {
i.cur = i.low
} else {
i.cur = ip.NextIP(i.cur)
}
// If we've looped back to where we started, exit
if i.cur.Equal(i.start) {
return nil
}
return i.cur
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 CNI authors
// Copyright 2017 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -16,12 +16,13 @@ package allocator
import (
"fmt"
"net"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
fakestore "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"net"
)
type AllocatorTestCase struct {
@ -31,31 +32,99 @@ type AllocatorTestCase struct {
lastIP string
}
func (t AllocatorTestCase) run() (*current.IPConfig, []*types.Route, error) {
func mkalloc() IPAllocator {
ipnet, _ := types.ParseCIDR("192.168.1.0/24")
r := Range{
Subnet: types.IPNet(*ipnet),
}
r.Canonicalize()
store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{})
alloc := IPAllocator{
netName: "netname",
ipRange: r,
store: store,
rangeID: "rangeid",
}
return alloc
}
func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
fmt.Fprintln(GinkgoWriter, "Index:", idx)
subnet, err := types.ParseCIDR(t.subnet)
if err != nil {
return nil, nil, err
return nil, err
}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
}
store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP))
alloc, err := NewIPAllocator(&conf, store)
if err != nil {
return nil, nil, err
}
res, routes, err := alloc.Get("ID")
if err != nil {
return nil, nil, err
conf := Range{
Subnet: types.IPNet(*subnet),
}
return res, routes, nil
Expect(conf.Canonicalize()).To(BeNil())
store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)})
alloc := IPAllocator{
"netname",
conf,
store,
"rangeid",
}
return alloc.Get("ID", nil)
}
var _ = Describe("host-local ip allocator", func() {
Context("RangeIter", func() {
It("should loop correctly from the beginning", func() {
r := RangeIter{
start: net.IP{10, 0, 0, 0},
low: net.IP{10, 0, 0, 0},
high: net.IP{10, 0, 0, 5},
}
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
Expect(r.Next()).To(BeNil())
})
It("should loop correctly from the end", func() {
r := RangeIter{
start: net.IP{10, 0, 0, 5},
low: net.IP{10, 0, 0, 0},
high: net.IP{10, 0, 0, 5},
}
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
Expect(r.Next()).To(BeNil())
})
It("should loop correctly from the middle", func() {
r := RangeIter{
start: net.IP{10, 0, 0, 3},
low: net.IP{10, 0, 0, 0},
high: net.IP{10, 0, 0, 5},
}
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
Expect(r.Next()).To(BeNil())
})
})
Context("when has free ip", func() {
It("should allocate ips in round robin", func() {
testCases := []AllocatorTestCase{
@ -66,6 +135,12 @@ var _ = Describe("host-local ip allocator", func() {
expectResult: "10.0.0.2",
lastIP: "",
},
{
subnet: "2001:db8:1::0/64",
ipmap: map[string]string{},
expectResult: "2001:db8:1::2",
lastIP: "",
},
{
subnet: "10.0.0.0/30",
ipmap: map[string]string{},
@ -128,209 +203,108 @@ var _ = Describe("host-local ip allocator", func() {
},
}
for _, tc := range testCases {
res, _, err := tc.run()
for idx, tc := range testCases {
res, err := tc.run(idx)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.IP.String()).To(Equal(tc.expectResult))
}
})
It("should not allocate the broadcast address", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
for i := 1; i < 254; i++ {
res, _, err := alloc.Get("ID")
alloc := mkalloc()
for i := 2; i < 255; i++ {
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped
s := fmt.Sprintf("192.168.1.%d/24", i+1)
s := fmt.Sprintf("192.168.1.%d/24", i)
Expect(s).To(Equal(res.Address.String()))
fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String())
}
_, _, err = alloc.Get("ID")
x, err := alloc.Get("ID", nil)
fmt.Fprintln(GinkgoWriter, "got ip", x)
Expect(err).To(HaveOccurred())
})
It("should allocate in a round-robin fashion", func() {
alloc := mkalloc()
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.String()).To(Equal("192.168.1.2/24"))
err = alloc.Release("ID")
Expect(err).ToNot(HaveOccurred())
res, err = alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.String()).To(Equal("192.168.1.3/24"))
})
It("should allocate RangeStart first", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP("192.168.1.10"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
res, _, err := alloc.Get("ID")
alloc := mkalloc()
alloc.ipRange.RangeStart = net.IP{192, 168, 1, 10}
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.String()).To(Equal("192.168.1.10/24"))
res, _, err = alloc.Get("ID")
res, err = alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.String()).To(Equal("192.168.1.11/24"))
})
It("should allocate RangeEnd but not past RangeEnd", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeEnd: net.ParseIP("192.168.1.5"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
alloc := mkalloc()
alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
for i := 1; i < 5; i++ {
res, _, err := alloc.Get("ID")
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped
Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1)))
}
_, _, err = alloc.Get("ID")
_, err := alloc.Get("ID", nil)
Expect(err).To(HaveOccurred())
})
Context("when requesting a specific IP", func() {
It("must allocate the requested IP", func() {
subnet, err := types.ParseCIDR("10.0.0.0/29")
Expect(err).ToNot(HaveOccurred())
requestedIP := net.ParseIP("10.0.0.2")
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: requestedIP},
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
res, _, err := alloc.Get("ID")
alloc := mkalloc()
requestedIP := net.IP{192, 168, 1, 5}
res, err := alloc.Get("ID", requestedIP)
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
})
It("must return an error when the requested IP is after RangeEnd", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
It("must fail when the requested IP is allocated", func() {
alloc := mkalloc()
requestedIP := net.IP{192, 168, 1, 5}
res, err := alloc.Get("ID", requestedIP)
Expect(err).ToNot(HaveOccurred())
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: net.ParseIP("192.168.1.50")},
RangeEnd: net.ParseIP("192.168.1.20"),
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
_, _, err = alloc.Get("ID")
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
_, err = alloc.Get("ID", requestedIP)
Expect(err).To(MatchError(`requested IP address "192.168.1.5" is not available in network: netname 192.168.1.0/24`))
})
It("must return an error when the requested IP is after RangeEnd", func() {
alloc := mkalloc()
alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
requestedIP := net.IP{192, 168, 1, 6}
_, err := alloc.Get("ID", requestedIP)
Expect(err).To(HaveOccurred())
})
It("must return an error when the requested IP is before RangeStart", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: net.ParseIP("192.168.1.3")},
RangeStart: net.ParseIP("192.168.1.10"),
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
_, _, err = alloc.Get("ID")
alloc := mkalloc()
alloc.ipRange.RangeStart = net.IP{192, 168, 1, 6}
requestedIP := net.IP{192, 168, 1, 5}
_, err := alloc.Get("ID", requestedIP)
Expect(err).To(HaveOccurred())
})
})
It("RangeStart must be in the given subnet", func() {
testcases := []struct {
name string
ipnet string
start string
}{
{"outside-subnet", "192.168.1.0/24", "10.0.0.1"},
{"zero-ip", "10.1.0.0/16", "10.1.0.0"},
}
for _, tc := range testcases {
subnet, err := types.ParseCIDR(tc.ipnet)
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: tc.name,
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP(tc.start),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
}
})
It("RangeEnd must be in the given subnet", func() {
testcases := []struct {
name string
ipnet string
end string
}{
{"outside-subnet", "192.168.1.0/24", "10.0.0.1"},
{"broadcast-ip", "10.1.0.0/16", "10.1.255.255"},
}
for _, tc := range testcases {
subnet, err := types.ParseCIDR(tc.ipnet)
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: tc.name,
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeEnd: net.ParseIP(tc.end),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
}
})
It("RangeEnd must be after RangeStart in the given subnet", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP("192.168.1.10"),
RangeEnd: net.ParseIP("192.168.1.3"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
})
Context("when out of ips", func() {
It("returns a meaningful error", func() {
testCases := []AllocatorTestCase{
@ -353,26 +327,10 @@ var _ = Describe("host-local ip allocator", func() {
},
},
}
for _, tc := range testCases {
_, _, err := tc.run()
Expect(err).To(MatchError("no IP addresses available in network: test"))
for idx, tc := range testCases {
_, err := tc.run(idx)
Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet))
}
})
})
Context("when given an invalid subnet", func() {
It("returns a meaningful error", func() {
subnet, err := types.ParseCIDR("192.168.1.0/31")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
})
})

View File

@ -20,35 +20,51 @@ import (
"net"
"github.com/containernetworking/cni/pkg/types"
types020 "github.com/containernetworking/cni/pkg/types/020"
)
// IPAMConfig represents the IP related network configuration.
// This nests Range because we initially only supported a single
// range directly, and wish to preserve backwards compatability
type IPAMConfig struct {
*Range
Name string
Type string `json:"type"`
RangeStart net.IP `json:"rangeStart"`
RangeEnd net.IP `json:"rangeEnd"`
Subnet types.IPNet `json:"subnet"`
Gateway net.IP `json:"gateway"`
Routes []types.Route `json:"routes"`
DataDir string `json:"dataDir"`
ResolvConf string `json:"resolvConf"`
Args *IPAMArgs `json:"-"`
Type string `json:"type"`
Routes []*types.Route `json:"routes"`
DataDir string `json:"dataDir"`
ResolvConf string `json:"resolvConf"`
Ranges []Range `json:"ranges"`
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
}
type IPAMArgs struct {
type IPAMEnvArgs struct {
types.CommonArgs
IP net.IP `json:"ip,omitempty"`
}
type IPAMArgs struct {
IPs []net.IP `json:"ips"`
}
// The top-level network config, just so we can get the IPAM block
type Net struct {
Name string `json:"name"`
CNIVersion string `json:"cniVersion"`
IPAM *IPAMConfig `json:"ipam"`
Args *struct {
A *IPAMArgs `json:"cni"`
} `json:"args"`
}
type Range struct {
RangeStart net.IP `json:"rangeStart,omitempty"` // The first ip, inclusive
RangeEnd net.IP `json:"rangeEnd,omitempty"` // The last ip, inclusive
Subnet types.IPNet `json:"subnet"`
Gateway net.IP `json:"gateway,omitempty"`
}
// NewIPAMConfig creates a NetworkConfig from the given network name.
func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) {
func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
n := Net{}
if err := json.Unmarshal(bytes, &n); err != nil {
return nil, "", err
@ -58,12 +74,71 @@ func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) {
return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
}
if args != "" {
n.IPAM.Args = &IPAMArgs{}
err := types.LoadArgs(args, n.IPAM.Args)
// Parse custom IP from both env args *and* the top-level args config
if envArgs != "" {
e := IPAMEnvArgs{}
err := types.LoadArgs(envArgs, &e)
if err != nil {
return nil, "", err
}
if e.IP != nil {
n.IPAM.IPArgs = []net.IP{e.IP}
}
}
if n.Args != nil && n.Args.A != nil && len(n.Args.A.IPs) != 0 {
n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
}
for idx, _ := range n.IPAM.IPArgs {
if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
return nil, "", fmt.Errorf("cannot understand ip: %v", err)
}
}
// If a single range (old-style config) is specified, move it to
// the Ranges array
if n.IPAM.Range != nil && n.IPAM.Range.Subnet.IP != nil {
n.IPAM.Ranges = append([]Range{*n.IPAM.Range}, n.IPAM.Ranges...)
}
n.IPAM.Range = nil
if len(n.IPAM.Ranges) == 0 {
return nil, "", fmt.Errorf("no IP ranges specified")
}
// Validate all ranges
numV4 := 0
numV6 := 0
for i, _ := range n.IPAM.Ranges {
if err := n.IPAM.Ranges[i].Canonicalize(); err != nil {
return nil, "", fmt.Errorf("Cannot understand range %d: %v", i, err)
}
if len(n.IPAM.Ranges[i].RangeStart) == 4 {
numV4++
} else {
numV6++
}
}
// CNI spec 0.2.0 and below supported only one v4 and v6 address
if numV4 > 1 || numV6 > 1 {
for _, v := range types020.SupportedVersions {
if n.CNIVersion == v {
return nil, "", fmt.Errorf("CNI version %v does not support more than 1 range per address family", n.CNIVersion)
}
}
}
// Check for overlaps
l := len(n.IPAM.Ranges)
for i, r1 := range n.IPAM.Ranges[:l-1] {
for j, r2 := range n.IPAM.Ranges[i+1:] {
if r1.Overlaps(&r2) {
return nil, "", fmt.Errorf("Range %d overlaps with range %d", i, (i + j + 1))
}
}
}
// Copy net name into IPAM so not to drag Net struct around
@ -71,14 +146,3 @@ func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) {
return n.IPAM, n.CNIVersion, nil
}
func convertRoutesToCurrent(routes []types.Route) []*types.Route {
var currentRoutes []*types.Route
for _, r := range routes {
currentRoutes = append(currentRoutes, &types.Route{
Dst: r.Dst,
GW: r.GW,
})
}
return currentRoutes
}

View File

@ -0,0 +1,309 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package allocator
import (
"net"
"github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("IPAM config", func() {
It("Should parse an old-style config", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.9",
"rangeEnd": "10.1.2.20",
"gateway": "10.1.2.30"
}
}`
conf, version, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
Expect(version).Should(Equal("0.3.1"))
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
Ranges: []Range{
{
RangeStart: net.IP{10, 1, 2, 9},
RangeEnd: net.IP{10, 1, 2, 20},
Gateway: net.IP{10, 1, 2, 30},
Subnet: types.IPNet{
IP: net.IP{10, 1, 2, 0},
Mask: net.CIDRMask(24, 32),
},
},
},
}))
})
It("Should parse a new-style config", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.9",
"rangeEnd": "10.1.2.20",
"gateway": "10.1.2.30"
},
{
"subnet": "11.1.2.0/24",
"rangeStart": "11.1.2.9",
"rangeEnd": "11.1.2.20",
"gateway": "11.1.2.30"
}
]
}
}`
conf, version, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
Expect(version).Should(Equal("0.3.1"))
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
Ranges: []Range{
{
RangeStart: net.IP{10, 1, 2, 9},
RangeEnd: net.IP{10, 1, 2, 20},
Gateway: net.IP{10, 1, 2, 30},
Subnet: types.IPNet{
IP: net.IP{10, 1, 2, 0},
Mask: net.CIDRMask(24, 32),
},
},
{
RangeStart: net.IP{11, 1, 2, 9},
RangeEnd: net.IP{11, 1, 2, 20},
Gateway: net.IP{11, 1, 2, 30},
Subnet: types.IPNet{
IP: net.IP{11, 1, 2, 0},
Mask: net.CIDRMask(24, 32),
},
},
},
}))
})
It("Should parse a mixed config", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.9",
"rangeEnd": "10.1.2.20",
"gateway": "10.1.2.30",
"ranges": [
{
"subnet": "11.1.2.0/24",
"rangeStart": "11.1.2.9",
"rangeEnd": "11.1.2.20",
"gateway": "11.1.2.30"
}
]
}
}`
conf, version, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
Expect(version).Should(Equal("0.3.1"))
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
Ranges: []Range{
{
RangeStart: net.IP{10, 1, 2, 9},
RangeEnd: net.IP{10, 1, 2, 20},
Gateway: net.IP{10, 1, 2, 30},
Subnet: types.IPNet{
IP: net.IP{10, 1, 2, 0},
Mask: net.CIDRMask(24, 32),
},
},
{
RangeStart: net.IP{11, 1, 2, 9},
RangeEnd: net.IP{11, 1, 2, 20},
Gateway: net.IP{11, 1, 2, 30},
Subnet: types.IPNet{
IP: net.IP{11, 1, 2, 0},
Mask: net.CIDRMask(24, 32),
},
},
},
}))
})
It("Should parse CNI_ARGS env", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.9",
"rangeEnd": "10.1.2.20",
"gateway": "10.1.2.30"
},
{
"subnet": "11.1.2.0/24",
"rangeStart": "11.1.2.9",
"rangeEnd": "11.1.2.20",
"gateway": "11.1.2.30"
}
]
}
}`
envArgs := "IP=10.1.2.10"
conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
Expect(err).NotTo(HaveOccurred())
Expect(conf.IPArgs).To(Equal([]net.IP{{10, 1, 2, 10}}))
})
It("Should parse config args", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"args": {
"cni": {
"ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"]
}
},
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.9",
"rangeEnd": "10.1.2.20",
"gateway": "10.1.2.30"
},
{
"subnet": "11.1.2.0/24",
"rangeStart": "11.1.2.9",
"rangeEnd": "11.1.2.20",
"gateway": "11.1.2.30"
},
{
"subnet": "2001:db8:1::/64"
}
]
}
}`
envArgs := "IP=10.1.2.10"
conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
Expect(err).NotTo(HaveOccurred())
Expect(conf.IPArgs).To(Equal([]net.IP{
{10, 1, 2, 10},
{10, 1, 2, 11},
{11, 11, 11, 11},
net.ParseIP("2001:db8:1::11"),
}))
})
It("Should detect overlap", func() {
input := `{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24",
"rangeEnd": "10.1.2.128"
},
{
"subnet": "10.1.2.0/24",
"rangeStart": "10.1.2.15"
}
]
}
}`
_, _, err := LoadIPAMConfig([]byte(input), "")
Expect(err).To(MatchError("Range 0 overlaps with range 1"))
})
It("Should should error on too many ranges", func() {
input := `{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24"
},
{
"subnet": "11.1.2.0/24"
}
]
}
}`
_, _, err := LoadIPAMConfig([]byte(input), "")
Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 range per address family"))
})
It("Should allow one v4 and v6 range for 0.2.0", func() {
input := `{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"ranges": [
{
"subnet": "10.1.2.0/24"
},
{
"subnet": "2001:db8:1::/24"
}
]
}
}`
_, _, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
})
})

View File

@ -0,0 +1,159 @@
// Copyright 2017 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package allocator
import (
"fmt"
"net"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/plugins/pkg/ip"
)
// Canonicalize takes a given range and ensures that all information is consistent,
// filling out Start, End, and Gateway with sane values if missing
func (r *Range) Canonicalize() error {
if err := canonicalizeIP(&r.Subnet.IP); err != nil {
return err
}
// Can't create an allocator for a network with no addresses, eg
// a /32 or /31
ones, masklen := r.Subnet.Mask.Size()
if ones > masklen-2 {
return fmt.Errorf("Network %s too small to allocate from", (*net.IPNet)(&r.Subnet).String())
}
if len(r.Subnet.IP) != len(r.Subnet.Mask) {
return fmt.Errorf("IPNet IP and Mask version mismatch")
}
// If the gateway is nil, claim .1
if r.Gateway == nil {
r.Gateway = ip.NextIP(r.Subnet.IP)
} else {
if err := canonicalizeIP(&r.Gateway); err != nil {
return err
}
subnet := (net.IPNet)(r.Subnet)
if !subnet.Contains(r.Gateway) {
return fmt.Errorf("gateway %s not in network %s", r.Gateway.String(), subnet.String())
}
}
// RangeStart: If specified, make sure it's sane (inside the subnet),
// otherwise use the first free IP (i.e. .1) - this will conflict with the
// gateway but we skip it in the iterator
if r.RangeStart != nil {
if err := canonicalizeIP(&r.RangeStart); err != nil {
return err
}
if err := r.IPInRange(r.RangeStart); err != nil {
return err
}
} else {
r.RangeStart = ip.NextIP(r.Subnet.IP)
}
// RangeEnd: If specified, verify sanity. Otherwise, add a sensible default
// (e.g. for a /24: .254 if IPv4, ::255 if IPv6)
if r.RangeEnd != nil {
if err := canonicalizeIP(&r.RangeEnd); err != nil {
return err
}
if err := r.IPInRange(r.RangeEnd); err != nil {
return err
}
} else {
r.RangeEnd = lastIP(r.Subnet)
}
return nil
}
// IsValidIP checks if a given ip is a valid, allocatable address in a given Range
func (r *Range) IPInRange(addr net.IP) error {
if err := canonicalizeIP(&addr); err != nil {
return err
}
subnet := (net.IPNet)(r.Subnet)
if len(addr) != len(r.Subnet.IP) {
return fmt.Errorf("IP %s is not the same protocol as subnet %s",
addr, subnet.String())
}
if !subnet.Contains(addr) {
return fmt.Errorf("%s not in network %s", addr, subnet.String())
}
// We ignore nils here so we can use this function as we initialize the range.
if r.RangeStart != nil {
if ip.Cmp(addr, r.RangeStart) < 0 {
return fmt.Errorf("%s is in network %s but before start %s",
addr, (*net.IPNet)(&r.Subnet).String(), r.RangeStart)
}
}
if r.RangeEnd != nil {
if ip.Cmp(addr, r.RangeEnd) > 0 {
return fmt.Errorf("%s is in network %s but after end %s",
addr, (*net.IPNet)(&r.Subnet).String(), r.RangeEnd)
}
}
return nil
}
// Overlaps returns true if there is any overlap between ranges
func (r *Range) Overlaps(r1 *Range) bool {
// different familes
if len(r.RangeStart) != len(r1.RangeStart) {
return false
}
return r.IPInRange(r1.RangeStart) == nil ||
r.IPInRange(r1.RangeEnd) == nil ||
r1.IPInRange(r.RangeStart) == nil ||
r1.IPInRange(r.RangeEnd) == nil
}
// canonicalizeIP makes sure a provided ip is in standard form
func canonicalizeIP(ip *net.IP) error {
if ip.To4() != nil {
*ip = ip.To4()
return nil
} else if ip.To16() != nil {
*ip = ip.To16()
return nil
}
return fmt.Errorf("IP %s not v4 nor v6", *ip)
}
// Determine the last IP of a subnet, excluding the broadcast if IPv4
func lastIP(subnet types.IPNet) net.IP {
var end net.IP
for i := 0; i < len(subnet.IP); i++ {
end = append(end, subnet.IP[i]|^subnet.Mask[i])
}
if subnet.IP.To4() != nil {
end[3]--
}
return end
}

View File

@ -0,0 +1,215 @@
// Copyright 2017 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package allocator
import (
"net"
"github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)
var _ = Describe("IP ranges", func() {
It("should generate sane defaults for ipv4", func() {
snstr := "192.0.2.0/24"
r := Range{Subnet: mustSubnet(snstr)}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r).To(Equal(Range{
Subnet: mustSubnet(snstr),
RangeStart: net.IP{192, 0, 2, 1},
RangeEnd: net.IP{192, 0, 2, 254},
Gateway: net.IP{192, 0, 2, 1},
}))
})
It("should generate sane defaults for a smaller ipv4 subnet", func() {
snstr := "192.0.2.0/25"
r := Range{Subnet: mustSubnet(snstr)}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r).To(Equal(Range{
Subnet: mustSubnet(snstr),
RangeStart: net.IP{192, 0, 2, 1},
RangeEnd: net.IP{192, 0, 2, 126},
Gateway: net.IP{192, 0, 2, 1},
}))
})
It("should generate sane defaults for ipv6", func() {
snstr := "2001:DB8:1::/64"
r := Range{Subnet: mustSubnet(snstr)}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r).To(Equal(Range{
Subnet: mustSubnet(snstr),
RangeStart: net.ParseIP("2001:DB8:1::1"),
RangeEnd: net.ParseIP("2001:DB8:1::ffff:ffff:ffff:ffff"),
Gateway: net.ParseIP("2001:DB8:1::1"),
}))
})
It("Should reject a network that's too small", func() {
r := Range{Subnet: mustSubnet("192.0.2.0/31")}
err := r.Canonicalize()
Expect(err).Should(MatchError("Network 192.0.2.0/31 too small to allocate from"))
})
It("should reject invalid RangeStart and RangeEnd specifications", func() {
r := Range{Subnet: mustSubnet("192.0.2.0/24"), RangeStart: net.ParseIP("192.0.3.0")}
err := r.Canonicalize()
Expect(err).Should(MatchError("192.0.3.0 not in network 192.0.2.0/24"))
r = Range{Subnet: mustSubnet("192.0.2.0/24"), RangeEnd: net.ParseIP("192.0.4.0")}
err = r.Canonicalize()
Expect(err).Should(MatchError("192.0.4.0 not in network 192.0.2.0/24"))
r = Range{
Subnet: mustSubnet("192.0.2.0/24"),
RangeStart: net.ParseIP("192.0.2.50"),
RangeEnd: net.ParseIP("192.0.2.40"),
}
err = r.Canonicalize()
Expect(err).Should(MatchError("192.0.2.50 is in network 192.0.2.0/24 but after end 192.0.2.40"))
})
It("should reject invalid gateways", func() {
r := Range{Subnet: mustSubnet("192.0.2.0/24"), Gateway: net.ParseIP("192.0.3.0")}
err := r.Canonicalize()
Expect(err).Should(MatchError("gateway 192.0.3.0 not in network 192.0.2.0/24"))
})
It("should parse all fields correctly", func() {
r := Range{
Subnet: mustSubnet("192.0.2.0/24"),
RangeStart: net.ParseIP("192.0.2.40"),
RangeEnd: net.ParseIP("192.0.2.50"),
Gateway: net.ParseIP("192.0.2.254"),
}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r).To(Equal(Range{
Subnet: mustSubnet("192.0.2.0/24"),
RangeStart: net.IP{192, 0, 2, 40},
RangeEnd: net.IP{192, 0, 2, 50},
Gateway: net.IP{192, 0, 2, 254},
}))
})
It("should accept v4 IPs in range and reject IPs out of range", func() {
r := Range{
Subnet: mustSubnet("192.0.2.0/24"),
RangeStart: net.ParseIP("192.0.2.40"),
RangeEnd: net.ParseIP("192.0.2.50"),
Gateway: net.ParseIP("192.0.2.254"),
}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r.IPInRange(net.ParseIP("192.0.3.0"))).Should(MatchError(
"192.0.3.0 not in network 192.0.2.0/24"))
Expect(r.IPInRange(net.ParseIP("192.0.2.39"))).Should(MatchError(
"192.0.2.39 is in network 192.0.2.0/24 but before start 192.0.2.40"))
Expect(r.IPInRange(net.ParseIP("192.0.2.40"))).Should(BeNil())
Expect(r.IPInRange(net.ParseIP("192.0.2.50"))).Should(BeNil())
Expect(r.IPInRange(net.ParseIP("192.0.2.51"))).Should(MatchError(
"192.0.2.51 is in network 192.0.2.0/24 but after end 192.0.2.50"))
})
It("should accept v6 IPs in range and reject IPs out of range", func() {
r := Range{
Subnet: mustSubnet("2001:DB8:1::/64"),
RangeStart: net.ParseIP("2001:db8:1::40"),
RangeEnd: net.ParseIP("2001:db8:1::50"),
}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
Expect(r.IPInRange(net.ParseIP("2001:db8:2::"))).Should(MatchError(
"2001:db8:2:: not in network 2001:db8:1::/64"))
Expect(r.IPInRange(net.ParseIP("2001:db8:1::39"))).Should(MatchError(
"2001:db8:1::39 is in network 2001:db8:1::/64 but before start 2001:db8:1::40"))
Expect(r.IPInRange(net.ParseIP("2001:db8:1::40"))).Should(BeNil())
Expect(r.IPInRange(net.ParseIP("2001:db8:1::50"))).Should(BeNil())
Expect(r.IPInRange(net.ParseIP("2001:db8:1::51"))).Should(MatchError(
"2001:db8:1::51 is in network 2001:db8:1::/64 but after end 2001:db8:1::50"))
})
DescribeTable("Detecting overlap",
func(r1 Range, r2 Range, expected bool) {
r1.Canonicalize()
r2.Canonicalize()
// operation should be commutative
Expect(r1.Overlaps(&r2)).To(Equal(expected))
Expect(r2.Overlaps(&r1)).To(Equal(expected))
},
Entry("non-overlapping",
Range{Subnet: mustSubnet("10.0.0.0/24")},
Range{Subnet: mustSubnet("10.0.1.0/24")},
false),
Entry("different families",
// Note that the bits overlap
Range{Subnet: mustSubnet("0.0.0.0/24")},
Range{Subnet: mustSubnet("::/24")},
false),
Entry("Identical",
Range{Subnet: mustSubnet("10.0.0.0/24")},
Range{Subnet: mustSubnet("10.0.0.0/24")},
true),
Entry("Containing",
Range{Subnet: mustSubnet("10.0.0.0/20")},
Range{Subnet: mustSubnet("10.0.1.0/24")},
true),
Entry("same subnet, non overlapping start + end",
Range{
Subnet: mustSubnet("10.0.0.0/24"),
RangeEnd: net.ParseIP("10.0.0.127"),
},
Range{
Subnet: mustSubnet("10.0.0.0/24"),
RangeStart: net.ParseIP("10.0.0.128"),
},
false),
Entry("same subnet, overlapping start + end",
Range{
Subnet: mustSubnet("10.0.0.0/24"),
RangeEnd: net.ParseIP("10.0.0.127"),
},
Range{
Subnet: mustSubnet("10.0.0.0/24"),
RangeStart: net.ParseIP("10.0.0.127"),
},
true),
)
})
func mustSubnet(s string) types.IPNet {
n, err := types.ParseCIDR(s)
if err != nil {
Fail(err.Error())
}
canonicalizeIP(&n.IP)
return types.IPNet(*n)
}

View File

@ -24,10 +24,12 @@ import (
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
)
const lastIPFile = "last_reserved_ip"
const lastIPFilePrefix = "last_reserved_ip."
var defaultDataDir = "/var/lib/cni/networks"
// Store is a simple disk-backed store that creates one file per IP
// address in a given directory. The contents of the file are the container ID.
type Store struct {
FileLock
dataDir string
@ -41,7 +43,7 @@ func New(network, dataDir string) (*Store, error) {
dataDir = defaultDataDir
}
dir := filepath.Join(dataDir, network)
if err := os.MkdirAll(dir, 0644); err != nil {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
@ -52,7 +54,7 @@ func New(network, dataDir string) (*Store, error) {
return &Store{*lk, dir}, nil
}
func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
fname := filepath.Join(s.dataDir, ip.String())
f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
if os.IsExist(err) {
@ -71,7 +73,7 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
return false, err
}
// store the reserved ip in lastIPFile
ipfile := filepath.Join(s.dataDir, lastIPFile)
ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
if err != nil {
return false, err
@ -80,8 +82,8 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
}
// LastReservedIP returns the last reserved IP if exists
func (s *Store) LastReservedIP() (net.IP, error) {
ipfile := filepath.Join(s.dataDir, lastIPFile)
func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
data, err := ioutil.ReadFile(ipfile)
if err != nil {
return nil, err

View File

@ -20,8 +20,8 @@ type Store interface {
Lock() error
Unlock() error
Close() error
Reserve(id string, ip net.IP) (bool, error)
LastReservedIP() (net.IP, error)
Reserve(id string, ip net.IP, rangeID string) (bool, error)
LastReservedIP(rangeID string) (net.IP, error)
Release(ip net.IP) error
ReleaseByID(id string) error
}

View File

@ -16,20 +16,21 @@ package testing
import (
"net"
"os"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
)
type FakeStore struct {
ipMap map[string]string
lastReservedIP net.IP
lastReservedIP map[string]net.IP
}
// FakeStore implements the Store interface
var _ backend.Store = &FakeStore{}
func NewFakeStore(ipmap map[string]string, lastIP net.IP) *FakeStore {
return &FakeStore{ipmap, lastIP}
func NewFakeStore(ipmap map[string]string, lastIPs map[string]net.IP) *FakeStore {
return &FakeStore{ipmap, lastIPs}
}
func (s *FakeStore) Lock() error {
@ -44,18 +45,22 @@ func (s *FakeStore) Close() error {
return nil
}
func (s *FakeStore) Reserve(id string, ip net.IP) (bool, error) {
func (s *FakeStore) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
key := ip.String()
if _, ok := s.ipMap[key]; !ok {
s.ipMap[key] = id
s.lastReservedIP = ip
s.lastReservedIP[rangeID] = ip
return true, nil
}
return false, nil
}
func (s *FakeStore) LastReservedIP() (net.IP, error) {
return s.lastReservedIP, nil
func (s *FakeStore) LastReservedIP(rangeID string) (net.IP, error) {
ip, ok := s.lastReservedIP[rangeID]
if !ok {
return nil, os.ErrNotExist
}
return ip, nil
}
func (s *FakeStore) Release(ip net.IP) error {
@ -75,3 +80,7 @@ func (s *FakeStore) ReleaseByID(id string) error {
}
return nil
}
func (s *FakeStore) SetIPMap(m map[string]string) {
s.ipMap = m
}

View File

@ -33,7 +33,7 @@ import (
)
var _ = Describe("host-local Operations", func() {
It("allocates and releases an address with ADD/DEL", func() {
It("allocates and releases addresses with ADD/DEL", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
@ -51,9 +51,18 @@ var _ = Describe("host-local Operations", func() {
"master": "foo0",
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s",
"resolvConf": "%s/resolv.conf"
"resolvConf": "%s/resolv.conf",
"ranges": [
{ "subnet": "10.1.2.0/24" },
{ "subnet": "2001:db8:1::0/64" }
],
"routes": [
{"dst": "0.0.0.0/0"},
{"dst": "::/0"},
{"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
{"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
]
}
}`, tmpDir, tmpDir)
@ -74,30 +83,60 @@ var _ = Describe("host-local Operations", func() {
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
Expect(err).NotTo(HaveOccurred())
Expect(len(result.IPs)).To(Equal(1))
expectedAddress.IP = expectedAddress.IP.To16()
Expect(result.IPs[0].Address).To(Equal(*expectedAddress))
Expect(result.IPs[0].Gateway).To(Equal(net.ParseIP("10.1.2.1")))
// Gomega is cranky about slices with different caps
Expect(*result.IPs[0]).To(Equal(
current.IPConfig{
Version: "4",
Interface: 0,
Address: mustCIDR("10.1.2.2/24"),
Gateway: net.ParseIP("10.1.2.1"),
}))
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
contents, err := ioutil.ReadFile(ipFilePath)
Expect(*result.IPs[1]).To(Equal(
current.IPConfig{
Version: "6",
Interface: 0,
Address: mustCIDR("2001:db8:1::2/64"),
Gateway: net.ParseIP("2001:db8:1::1"),
},
))
Expect(len(result.IPs)).To(Equal(2))
Expect(result.Routes).To(Equal([]*types.Route{
&types.Route{Dst: mustCIDR("0.0.0.0/0"), GW: nil},
&types.Route{Dst: mustCIDR("::/0"), GW: nil},
&types.Route{Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("1.1.1.1")},
&types.Route{Dst: mustCIDR("2001:db8:2::0/64"), GW: net.ParseIP("2001:db8:3::1")},
}))
ipFilePath1 := filepath.Join(tmpDir, "mynet", "10.1.2.2")
contents, err := ioutil.ReadFile(ipFilePath1)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
contents, err = ioutil.ReadFile(lastFilePath)
ipFilePath2 := filepath.Join(tmpDir, "mynet", "2001:db8:1::2")
contents, err = ioutil.ReadFile(ipFilePath2)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
contents, err = ioutil.ReadFile(lastFilePath1)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.IAENuAABAAAAAAAAAAAAAQ==")
contents, err = ioutil.ReadFile(lastFilePath2)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("2001:db8:1::2"))
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(ipFilePath)
_, err = os.Stat(ipFilePath1)
Expect(err).To(HaveOccurred())
_, err = os.Stat(ipFilePath2)
Expect(err).To(HaveOccurred())
})
@ -187,7 +226,7 @@ var _ = Describe("host-local Operations", func() {
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
contents, err = ioutil.ReadFile(lastFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
@ -289,4 +328,203 @@ var _ = Describe("host-local Operations", func() {
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(out), "Error retriving last reserved ip")).To(Equal(-1))
})
It("allocates a custom IP when requested by config args", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"dataDir": "%s",
"ranges": [
{ "subnet": "10.1.2.0/24" }
]
},
"args": {
"cni": {
"ips": ["10.1.2.88"]
}
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(1))
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
})
It("allocates custom IPs from multiple ranges", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"dataDir": "%s",
"ranges": [
{ "subnet": "10.1.2.0/24" },
{ "subnet": "10.1.3.0/24" }
]
},
"args": {
"cni": {
"ips": ["10.1.2.88", "10.1.3.77"]
}
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(2))
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("10.1.3.77")))
})
It("allocates custom IPs from multiple protocols", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"dataDir": "%s",
"ranges": [
{ "subnet": "10.1.2.0/24" },
{ "subnet": "2001:db8:1::/24" }
]
},
"args": {
"cni": {
"ips": ["10.1.2.88", "2001:db8:1::999"]
}
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(2))
Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("2001:db8:1::999")))
})
It("fails if a requested custom IP is not used", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
"cniVersion": "0.3.1",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"dataDir": "%s",
"ranges": [
{ "subnet": "10.1.2.0/24" },
{ "subnet": "10.1.3.0/24" }
]
},
"args": {
"cni": {
"ips": ["10.1.2.88", "10.1.2.77"]
}
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
_, _, err = testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).To(HaveOccurred())
// Need to match prefix, because ordering is not guaranteed
Expect(err.Error()).To(HavePrefix("failed to allocate all requested IPs: 10.1.2."))
})
})
func mustCIDR(s string) net.IPNet {
ip, n, err := net.ParseCIDR(s)
n.IP = ip
if err != nil {
Fail(err.Error())
}
return *n
}

View File

@ -15,6 +15,10 @@
package main
import (
"fmt"
"net"
"strings"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
@ -50,17 +54,58 @@ func cmdAdd(args *skel.CmdArgs) error {
}
defer store.Close()
allocator, err := allocator.NewIPAllocator(ipamConf, store)
if err != nil {
return err
// Keep the allocators we used, so we can release all IPs if an error
// occurs after we start allocating
allocs := []*allocator.IPAllocator{}
// Store all requested IPs in a map, so we can easily remove ones we use
// and error if some remain
requestedIPs := map[string]net.IP{} //net.IP cannot be a key
for _, ip := range ipamConf.IPArgs {
requestedIPs[ip.String()] = ip
}
ipConf, routes, err := allocator.Get(args.ContainerID)
if err != nil {
return err
for idx, ipRange := range ipamConf.Ranges {
allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
// Check to see if there are any custom IPs requested in this range.
var requestedIP net.IP
for k, ip := range requestedIPs {
if ipRange.IPInRange(ip) == nil {
requestedIP = ip
delete(requestedIPs, k)
break
}
}
ipConf, err := allocator.Get(args.ContainerID, requestedIP)
if err != nil {
// Deallocate all already allocated IPs
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID)
}
return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
}
allocs = append(allocs, allocator)
result.IPs = append(result.IPs, ipConf)
}
result.IPs = []*current.IPConfig{ipConf}
result.Routes = routes
// If an IP was requested that wasn't fulfilled, fail
if len(requestedIPs) != 0 {
for _, alloc := range allocs {
_ = alloc.Release(args.ContainerID)
}
errstr := "failed to allocate all requested IPs:"
for _, ip := range requestedIPs {
errstr = errstr + " " + ip.String()
}
return fmt.Errorf(errstr)
}
result.Routes = ipamConf.Routes
return types.PrintResult(result, confVersion)
}
@ -77,10 +122,19 @@ func cmdDel(args *skel.CmdArgs) error {
}
defer store.Close()
ipAllocator, err := allocator.NewIPAllocator(ipamConf, store)
if err != nil {
return err
// Loop through all ranges, releasing all IPs, even if an error occurs
var errors []string
for _, ipRange := range ipamConf.Ranges {
ipAllocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
err := ipAllocator.Release(args.ContainerID)
if err != nil {
errors = append(errors, err.Error())
}
}
return ipAllocator.Release(args.ContainerID)
if errors != nil {
return fmt.Errorf(strings.Join(errors, ";"))
}
return nil
}