ipam/host-local: support sets of disjoint ranges
authorCasey Callendrello <casey.callendrello@coreos.com>
Mon, 31 Jul 2017 12:38:11 +0000 (14:38 +0200)
committerCasey Callendrello <casey.callendrello@coreos.com>
Wed, 9 Aug 2017 17:02:08 +0000 (19:02 +0200)
In real-world address allocations, disjoint address ranges are common.
Therefore, the host-local allocator should support them.

This change still allows for multiple IPs in a single configuration, but
also allows for a "set of subnets."

Fixes: #45

13 files changed:
plugins/ipam/host-local/README.md
plugins/ipam/host-local/backend/allocator/allocator.go
plugins/ipam/host-local/backend/allocator/allocator_test.go
plugins/ipam/host-local/backend/allocator/config.go
plugins/ipam/host-local/backend/allocator/config_test.go
plugins/ipam/host-local/backend/allocator/range.go
plugins/ipam/host-local/backend/allocator/range_set.go [new file with mode: 0644]
plugins/ipam/host-local/backend/allocator/range_set_test.go [new file with mode: 0644]
plugins/ipam/host-local/backend/allocator/range_test.go
plugins/ipam/host-local/host_local_test.go
plugins/ipam/host-local/main.go
plugins/main/bridge/bridge_test.go
plugins/main/ptp/ptp_test.go

index 7ce4a68..7deb355 100644 (file)
@@ -8,24 +8,40 @@ it can include a DNS configuration from a `resolv.conf` file on the host.
 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.
 
+The allocator can allocate multiple ranges, and supports sets of multiple (disjoint) 
+subnets. The allocation strategy is loosely round-robin within each range set.
+
 ## Example configurations
 
+Note that the key `ranges` is a list of range sets. That is to say, the length 
+of the top-level array is the number of addresses returned. The second-level 
+array is a set of subnets to use as a pool of possible addresses.
+
+This example configuration returns 2 IP addresses.
+
 ```json
 {
        "ipam": {
                "type": "host-local",
                "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"
-                       }
+                       [
+                               {
+                                       "subnet": "10.10.0.0/16",
+                                       "rangeStart": "10.10.1.20",
+                                       "rangeEnd": "10.10.3.50",
+                                       "gateway": "10.10.0.254"
+                               },
+                               {
+                                       "subnet": "172.16.5.0/24"
+                               }
+                       ],
+                       [
+                               {
+                                       "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" },
@@ -58,7 +74,7 @@ deprecated but still supported.
 We can test it out on the command-line:
 
 ```bash
-$ 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
+$ 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
 
 ```
 
@@ -86,7 +102,7 @@ $ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-l
 * `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:
+* `ranges`, (array, required, nonempty) an array of arrays 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
index d4c2797..1d2964b 100644 (file)
 package allocator
 
 import (
-       "encoding/base64"
        "fmt"
        "log"
        "net"
        "os"
+       "strconv"
 
        "github.com/containernetworking/cni/pkg/types/current"
        "github.com/containernetworking/plugins/pkg/ip"
@@ -27,29 +27,16 @@ import (
 )
 
 type IPAllocator struct {
-       netName string
-       ipRange Range
-       store   backend.Store
-       rangeID string // Used for tracking last reserved ip
+       rangeset *RangeSet
+       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
-}
-
-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)
-
+func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator {
        return &IPAllocator{
-               netName: netName,
-               ipRange: r,
-               store:   store,
-               rangeID: rangeID,
+               rangeset: s,
+               store:    store,
+               rangeID:  strconv.Itoa(id),
        }
 }
 
@@ -58,27 +45,32 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
        a.store.Lock()
        defer a.store.Unlock()
 
-       gw := a.ipRange.Gateway
-
-       var reservedIP net.IP
+       var reservedIP *net.IPNet
+       var gw net.IP
 
        if requestedIP != nil {
-               if gw != nil && gw.Equal(requestedIP) {
-                       return nil, fmt.Errorf("requested IP must differ from gateway IP")
+               if err := canonicalizeIP(&requestedIP); err != nil {
+                       return nil, err
                }
 
-               if err := a.ipRange.IPInRange(requestedIP); err != nil {
+               r, err := a.rangeset.RangeFor(requestedIP)
+               if err != nil {
                        return nil, err
                }
 
+               if requestedIP.Equal(r.Gateway) {
+                       return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
+               }
+
                reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
                if err != nil {
                        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())
+                       return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String())
                }
-               reservedIP = requestedIP
+               reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask}
+               gw = r.Gateway
 
        } else {
                iter, err := a.GetIter()
@@ -86,39 +78,33 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
                        return nil, err
                }
                for {
-                       cur := iter.Next()
-                       if cur == nil {
+                       reservedIP, gw = iter.Next()
+                       if reservedIP == nil {
                                break
                        }
 
-                       // don't allocate gateway IP
-                       if gw != nil && cur.Equal(gw) {
-                               continue
-                       }
-
-                       reserved, err := a.store.Reserve(id, cur, a.rangeID)
+                       reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID)
                        if err != nil {
                                return nil, err
                        }
 
                        if reserved {
-                               reservedIP = cur
                                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, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String())
        }
        version := "4"
-       if reservedIP.To4() == nil {
+       if reservedIP.IP.To4() == nil {
                version = "6"
        }
 
        return &current.IPConfig{
                Version: version,
-               Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask},
+               Address: *reservedIP,
                Gateway: gw,
        }, nil
 }
@@ -131,15 +117,28 @@ func (a *IPAllocator) Release(id string) error {
        return a.store.ReleaseByID(id)
 }
 
+type RangeIter struct {
+       rangeset *RangeSet
+
+       // The current range id
+       rangeIdx int
+
+       // Our current position
+       cur net.IP
+
+       // The IP and range index where we started iterating; if we hit this again, we're done.
+       startIP    net.IP
+       startRange int
+}
+
 // GetIter encapsulates the strategy for this allocator.
-// We use a round-robin strategy, attempting to evenly use the whole subnet.
+// We use a round-robin strategy, attempting to evenly use the whole set.
 // 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,
+       iter := RangeIter{
+               rangeset: a.rangeset,
        }
 
        // Round-robin by trying to allocate from the last reserved IP + 1
@@ -151,39 +150,68 @@ func (a *IPAllocator) GetIter() (*RangeIter, error) {
        if err != nil && !os.IsNotExist(err) {
                log.Printf("Error retrieving last reserved ip: %v", err)
        } else if lastReservedIP != nil {
-               startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
+               startFromLastReservedIP = a.rangeset.Contains(lastReservedIP)
        }
 
+       // Find the range in the set with this IP
        if startFromLastReservedIP {
-               if i.high.Equal(lastReservedIP) {
-                       i.start = i.low
-               } else {
-                       i.start = ip.NextIP(lastReservedIP)
+               for i, r := range *a.rangeset {
+                       if r.Contains(lastReservedIP) {
+                               iter.rangeIdx = i
+                               iter.startRange = i
+
+                               // We advance the cursor on every Next(), so the first call
+                               // to next() will return lastReservedIP + 1
+                               iter.cur = lastReservedIP
+                               break
+                       }
                }
        } else {
-               i.start = a.ipRange.RangeStart
+               iter.rangeIdx = 0
+               iter.startRange = 0
+               iter.startIP = (*a.rangeset)[0].RangeStart
        }
-       return &i, nil
+       return &iter, 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
+// Next returns the next IP, its mask, and its gateway. Returns nil
+// if the iterator has been exhausted
+func (i *RangeIter) Next() (*net.IPNet, net.IP) {
+       r := (*i.rangeset)[i.rangeIdx]
+
+       // If this is the first time iterating and we're not starting in the middle
+       // of the range, then start at rangeStart, which is inclusive
        if i.cur == nil {
-               i.cur = i.start
-               return i.cur
+               i.cur = r.RangeStart
+               i.startIP = i.cur
+               if i.cur.Equal(r.Gateway) {
+                       return i.Next()
+               }
+               return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
        }
-       //  we returned .high last time, since we're inclusive
-       if i.cur.Equal(i.high) {
-               i.cur = i.low
+
+       // If we've reached the end of this range, we need to advance the range
+       // RangeEnd is inclusive as well
+       if i.cur.Equal(r.RangeEnd) {
+               i.rangeIdx += 1
+               i.rangeIdx %= len(*i.rangeset)
+               r = (*i.rangeset)[i.rangeIdx]
+
+               i.cur = r.RangeStart
        } 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
+       if i.startIP == nil {
+               i.startIP = i.cur
+       } else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) {
+               // IF we've looped back to where we started, give up
+               return nil, nil
+       }
+
+       if i.cur.Equal(r.Gateway) {
+               return i.Next()
        }
 
-       return i.cur
+       return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
 }
index 2c258a9..436aaa5 100644 (file)
@@ -21,31 +21,29 @@ import (
        "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"
 )
 
 type AllocatorTestCase struct {
-       subnet       string
+       subnets      []string
        ipmap        map[string]string
        expectResult string
        lastIP       string
 }
 
 func mkalloc() IPAllocator {
-       ipnet, _ := types.ParseCIDR("192.168.1.0/24")
-
-       r := Range{
-               Subnet: types.IPNet(*ipnet),
+       p := RangeSet{
+               Range{Subnet: mustSubnet("192.168.1.0/29")},
        }
-       r.Canonicalize()
+       p.Canonicalize()
        store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{})
 
        alloc := IPAllocator{
-               netName: "netname",
-               ipRange: r,
-               store:   store,
-               rangeID: "rangeid",
+               rangeset: &p,
+               store:    store,
+               rangeID:  "rangeid",
        }
 
        return alloc
@@ -53,24 +51,23 @@ func mkalloc() IPAllocator {
 
 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, err
-       }
-
-       conf := Range{
-               Subnet: types.IPNet(*subnet),
+       p := RangeSet{}
+       for _, s := range t.subnets {
+               subnet, err := types.ParseCIDR(s)
+               if err != nil {
+                       return nil, err
+               }
+               p = append(p, Range{Subnet: types.IPNet(*subnet)})
        }
 
-       Expect(conf.Canonicalize()).To(BeNil())
+       Expect(p.Canonicalize()).To(BeNil())
 
        store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)})
 
        alloc := IPAllocator{
-               "netname",
-               conf,
-               store,
-               "rangeid",
+               rangeset: &p,
+               store:    store,
+               rangeID:  "rangeid",
        }
 
        return alloc.Get("ID", nil)
@@ -79,50 +76,40 @@ func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
 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())
+                       a := mkalloc()
+                       r, _ := a.GetIter()
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+                       Expect(r.nextip()).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())
+                       a := mkalloc()
+                       a.store.Reserve("ID", net.IP{192, 168, 1, 6}, a.rangeID)
+                       a.store.ReleaseByID("ID")
+                       r, _ := a.GetIter()
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+                       Expect(r.nextip()).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())
+                       a := mkalloc()
+                       a.store.Reserve("ID", net.IP{192, 168, 1, 3}, a.rangeID)
+                       a.store.ReleaseByID("ID")
+                       r, _ := a.GetIter()
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+                       Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+                       Expect(r.nextip()).To(BeNil())
                })
-
        })
 
        Context("when has free ip", func() {
@@ -130,25 +117,25 @@ var _ = Describe("host-local ip allocator", func() {
                        testCases := []AllocatorTestCase{
                                // fresh start
                                {
-                                       subnet:       "10.0.0.0/29",
+                                       subnets:      []string{"10.0.0.0/29"},
                                        ipmap:        map[string]string{},
                                        expectResult: "10.0.0.2",
                                        lastIP:       "",
                                },
                                {
-                                       subnet:       "2001:db8:1::0/64",
+                                       subnets:      []string{"2001:db8:1::0/64"},
                                        ipmap:        map[string]string{},
                                        expectResult: "2001:db8:1::2",
                                        lastIP:       "",
                                },
                                {
-                                       subnet:       "10.0.0.0/30",
+                                       subnets:      []string{"10.0.0.0/30"},
                                        ipmap:        map[string]string{},
                                        expectResult: "10.0.0.2",
                                        lastIP:       "",
                                },
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.2": "id",
                                        },
@@ -157,13 +144,13 @@ var _ = Describe("host-local ip allocator", func() {
                                },
                                // next ip of last reserved ip
                                {
-                                       subnet:       "10.0.0.0/29",
+                                       subnets:      []string{"10.0.0.0/29"},
                                        ipmap:        map[string]string{},
                                        expectResult: "10.0.0.6",
                                        lastIP:       "10.0.0.5",
                                },
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.4": "id",
                                                "10.0.0.5": "id",
@@ -173,7 +160,7 @@ var _ = Describe("host-local ip allocator", func() {
                                },
                                // round robin to the beginning
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.6": "id",
                                        },
@@ -182,16 +169,17 @@ var _ = Describe("host-local ip allocator", func() {
                                },
                                // lastIP is out of range
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.2": "id",
                                        },
                                        expectResult: "10.0.0.3",
                                        lastIP:       "10.0.0.128",
                                },
+                               // subnet is completely full except for lastip
                                // wrap around and reserve lastIP
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.2": "id",
                                                "10.0.0.4": "id",
@@ -201,6 +189,26 @@ var _ = Describe("host-local ip allocator", func() {
                                        expectResult: "10.0.0.3",
                                        lastIP:       "10.0.0.3",
                                },
+                               // alocate from multiple subnets
+                               {
+                                       subnets:      []string{"10.0.0.0/30", "10.0.1.0/30"},
+                                       expectResult: "10.0.0.2",
+                                       ipmap:        map[string]string{},
+                               },
+                               // advance to next subnet
+                               {
+                                       subnets:      []string{"10.0.0.0/30", "10.0.1.0/30"},
+                                       lastIP:       "10.0.0.2",
+                                       expectResult: "10.0.1.2",
+                                       ipmap:        map[string]string{},
+                               },
+                               // Roll to start subnet
+                               {
+                                       subnets:      []string{"10.0.0.0/30", "10.0.1.0/30", "10.0.2.0/30"},
+                                       lastIP:       "10.0.2.2",
+                                       expectResult: "10.0.0.2",
+                                       ipmap:        map[string]string{},
+                               },
                        }
 
                        for idx, tc := range testCases {
@@ -212,10 +220,10 @@ var _ = Describe("host-local ip allocator", func() {
 
                It("should not allocate the broadcast address", func() {
                        alloc := mkalloc()
-                       for i := 2; i < 255; i++ {
+                       for i := 2; i < 7; i++ {
                                res, err := alloc.Get("ID", nil)
                                Expect(err).ToNot(HaveOccurred())
-                               s := fmt.Sprintf("192.168.1.%d/24", i)
+                               s := fmt.Sprintf("192.168.1.%d/29", i)
                                Expect(s).To(Equal(res.Address.String()))
                                fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String())
                        }
@@ -229,44 +237,17 @@ var _ = Describe("host-local ip allocator", func() {
                        alloc := mkalloc()
                        res, err := alloc.Get("ID", nil)
                        Expect(err).ToNot(HaveOccurred())
-                       Expect(res.Address.String()).To(Equal("192.168.1.2/24"))
+                       Expect(res.Address.String()).To(Equal("192.168.1.2/29"))
 
                        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"))
+                       Expect(res.Address.String()).To(Equal("192.168.1.3/29"))
 
                })
 
-               It("should allocate RangeStart first", func() {
-                       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", 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() {
-                       alloc := mkalloc()
-                       alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
-
-                       for i := 1; i < 5; i++ {
-                               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", nil)
-                       Expect(err).To(HaveOccurred())
-               })
-
                Context("when requesting a specific IP", func() {
                        It("must allocate the requested IP", func() {
                                alloc := mkalloc()
@@ -284,21 +265,21 @@ var _ = Describe("host-local ip allocator", func() {
                                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`))
+                               Expect(err).To(MatchError(`requested IP address 192.168.1.5 is not available in range set 192.168.1.1-192.168.1.6`))
                        })
 
                        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}
+                               (*alloc.rangeset)[0].RangeEnd = net.IP{192, 168, 1, 4}
+                               requestedIP := net.IP{192, 168, 1, 5}
                                _, err := alloc.Get("ID", requestedIP)
                                Expect(err).To(HaveOccurred())
                        })
 
                        It("must return an error when the requested IP is before RangeStart", func() {
                                alloc := mkalloc()
-                               alloc.ipRange.RangeStart = net.IP{192, 168, 1, 6}
-                               requestedIP := net.IP{192, 168, 1, 5}
+                               (*alloc.rangeset)[0].RangeStart = net.IP{192, 168, 1, 3}
+                               requestedIP := net.IP{192, 168, 1, 2}
                                _, err := alloc.Get("ID", requestedIP)
                                Expect(err).To(HaveOccurred())
                        })
@@ -309,28 +290,44 @@ var _ = Describe("host-local ip allocator", func() {
                It("returns a meaningful error", func() {
                        testCases := []AllocatorTestCase{
                                {
-                                       subnet: "10.0.0.0/30",
+                                       subnets: []string{"10.0.0.0/30"},
                                        ipmap: map[string]string{
                                                "10.0.0.2": "id",
-                                               "10.0.0.3": "id",
                                        },
                                },
                                {
-                                       subnet: "10.0.0.0/29",
+                                       subnets: []string{"10.0.0.0/29"},
                                        ipmap: map[string]string{
                                                "10.0.0.2": "id",
                                                "10.0.0.3": "id",
                                                "10.0.0.4": "id",
                                                "10.0.0.5": "id",
                                                "10.0.0.6": "id",
-                                               "10.0.0.7": "id",
+                                       },
+                               },
+                               {
+                                       subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
+                                       ipmap: map[string]string{
+                                               "10.0.0.2": "id",
+                                               "10.0.1.2": "id",
                                        },
                                },
                        }
                        for idx, tc := range testCases {
                                _, err := tc.run(idx)
-                               Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet))
+                               Expect(err).NotTo(BeNil())
+                               Expect(err.Error()).To(HavePrefix("no IP addresses available in range set"))
                        }
                })
        })
 })
+
+// nextip is a convenience function used for testing
+func (i *RangeIter) nextip() net.IP {
+       c, _ := i.Next()
+       if c == nil {
+               return nil
+       }
+
+       return c.IP
+}
index 19e2f3c..845dad0 100644 (file)
@@ -23,6 +23,16 @@ import (
        types020 "github.com/containernetworking/cni/pkg/types/020"
 )
 
+// 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"`
+}
+
 // 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
@@ -33,7 +43,7 @@ type IPAMConfig struct {
        Routes     []*types.Route `json:"routes"`
        DataDir    string         `json:"dataDir"`
        ResolvConf string         `json:"resolvConf"`
-       Ranges     []Range        `json:"ranges"`
+       Ranges     []RangeSet     `json:"ranges"`
        IPArgs     []net.IP       `json:"-"` // Requested IPs from CNI_ARGS and args
 }
 
@@ -46,15 +56,7 @@ 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 RangeSet []Range
 
 type Range struct {
        RangeStart net.IP      `json:"rangeStart,omitempty"` // The first ip, inclusive
@@ -97,10 +99,10 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
                }
        }
 
-       // If a single range (old-style config) is specified, move it to
+       // If a single range (old-style config) is specified, prepend 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.Ranges = append([]RangeSet{{*n.IPAM.Range}}, n.IPAM.Ranges...)
        }
        n.IPAM.Range = nil
 
@@ -113,9 +115,10 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
        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)
+                       return nil, "", fmt.Errorf("invalid range set %d: %s", i, err)
                }
-               if len(n.IPAM.Ranges[i].RangeStart) == 4 {
+
+               if n.IPAM.Ranges[i][0].RangeStart.To4() != nil {
                        numV4++
                } else {
                        numV6++
@@ -126,17 +129,17 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
        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)
+                               return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per 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))
+       for i, p1 := range n.IPAM.Ranges[:l-1] {
+               for j, p2 := range n.IPAM.Ranges[i+1:] {
+                       if p1.Overlaps(&p2) {
+                               return nil, "", fmt.Errorf("range set %d overlaps with %d", i, (i + j + 1))
                        }
                }
        }
index b162ab4..4631123 100644 (file)
@@ -44,43 +44,51 @@ var _ = Describe("IPAM config", func() {
                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),
+                       Ranges: []RangeSet{
+                               RangeSet{
+                                       {
+                                               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"
+                       "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": "10.1.4.0/24"
+                                               }
+                                       ],
+                                       [{
+                                               "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"))
@@ -88,23 +96,36 @@ var _ = Describe("IPAM config", func() {
                Expect(conf).To(Equal(&IPAMConfig{
                        Name: "mynet",
                        Type: "host-local",
-                       Ranges: []Range{
+                       Ranges: []RangeSet{
                                {
-                                       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{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{10, 1, 4, 1},
+                                               RangeEnd:   net.IP{10, 1, 4, 254},
+                                               Gateway:    net.IP{10, 1, 4, 1},
+                                               Subnet: types.IPNet{
+                                                       IP:   net.IP{10, 1, 4, 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),
+                                       {
+                                               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),
+                                               },
                                        },
                                },
                        },
@@ -113,26 +134,26 @@ var _ = Describe("IPAM config", func() {
 
        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"
+                       "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"))
@@ -140,23 +161,27 @@ var _ = Describe("IPAM config", func() {
                Expect(conf).To(Equal(&IPAMConfig{
                        Name: "mynet",
                        Type: "host-local",
-                       Ranges: []Range{
+                       Ranges: []RangeSet{
                                {
-                                       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{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),
+                                       {
+                                               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),
+                                               },
                                        },
                                },
                        },
@@ -165,28 +190,22 @@ var _ = Describe("IPAM config", func() {
 
        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"
+                       "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"
+                                       }
+                               ]]
                        }
-               ]
-       }
-}`
+               }`
 
                envArgs := "IP=10.1.2.10"
 
@@ -195,38 +214,39 @@ var _ = Describe("IPAM config", func() {
                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"
+                       "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"]
+                               }
                        },
-                       {
-                               "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"
+                       "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"
 
@@ -239,70 +259,106 @@ var _ = Describe("IPAM config", func() {
                        net.ParseIP("2001:db8:1::11"),
                }))
        })
-       It("Should detect overlap", func() {
+
+       It("Should detect overlap between rangesets", 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"
+                       "cniVersion": "0.3.1",
+                       "name": "mynet",
+                       "type": "ipvlan",
+                       "master": "foo0",
+                       "ipam": {
+                               "type": "host-local",
+                               "ranges": [
+                                       [
+                                               {"subnet": "10.1.2.0/24"},
+                                               {"subnet": "10.2.2.0/24"}
+                                       ],
+                                       [
+                                               { "subnet": "10.1.4.0/24"},
+                                               { "subnet": "10.1.6.0/24"},
+                                               { "subnet": "10.1.8.0/24"},
+                                               { "subnet": "10.1.2.0/24"}
+                                       ]
+                               ]
                        }
-               ]
-       }
-}`
+               }`
                _, _, err := LoadIPAMConfig([]byte(input), "")
-               Expect(err).To(MatchError("Range 0 overlaps with range 1"))
+               Expect(err).To(MatchError("range set 0 overlaps with 1"))
        })
 
-       It("Should should error on too many ranges", func() {
+       It("Should detect overlap within rangeset", 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"
+                       "cniVersion": "0.3.1",
+                       "name": "mynet",
+                       "type": "ipvlan",
+                       "master": "foo0",
+                       "ipam": {
+                               "type": "host-local",
+                               "ranges": [
+                                       [
+                                               { "subnet": "10.1.0.0/22" },
+                                               { "subnet": "10.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"))
+               Expect(err).To(MatchError("invalid range set 0: subnets 10.1.0.1-10.1.3.254 and 10.1.2.1-10.1.2.254 overlap"))
        })
 
-       It("Should allow one v4 and v6 range for 0.2.0", func() {
+       It("should error on rangesets with different families", 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"
+                       "cniVersion": "0.3.1",
+                       "name": "mynet",
+                       "type": "ipvlan",
+                       "master": "foo0",
+                       "ipam": {
+                               "type": "host-local",
+                               "ranges": [
+                                       [
+                                               { "subnet": "10.1.0.0/22" },
+                                               { "subnet": "2001:db8:5::/64" }
+                                       ]
+                               ]
                        }
-               ]
-       }
-}`
+               }`
+               _, _, err := LoadIPAMConfig([]byte(input), "")
+               Expect(err).To(MatchError("invalid range set 0: mixed address families"))
+
+       })
+
+       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 address per 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())
        })
index 5c7a1dd..e696b02 100644 (file)
@@ -61,8 +61,8 @@ func (r *Range) Canonicalize() error {
                        return err
                }
 
-               if err := r.IPInRange(r.RangeStart); err != nil {
-                       return err
+               if !r.Contains(r.RangeStart) {
+                       return fmt.Errorf("RangeStart %s not in network %s", r.RangeStart.String(), (*net.IPNet)(&r.Subnet).String())
                }
        } else {
                r.RangeStart = ip.NextIP(r.Subnet.IP)
@@ -75,8 +75,8 @@ func (r *Range) Canonicalize() error {
                        return err
                }
 
-               if err := r.IPInRange(r.RangeEnd); err != nil {
-                       return err
+               if !r.Contains(r.RangeEnd) {
+                       return fmt.Errorf("RangeEnd %s not in network %s", r.RangeEnd.String(), (*net.IPNet)(&r.Subnet).String())
                }
        } else {
                r.RangeEnd = lastIP(r.Subnet)
@@ -86,38 +86,39 @@ func (r *Range) Canonicalize() error {
 }
 
 // IsValidIP checks if a given ip is a valid, allocatable address in a given Range
-func (r *Range) IPInRange(addr net.IP) error {
+func (r *Range) Contains(addr net.IP) bool {
        if err := canonicalizeIP(&addr); err != nil {
-               return err
+               return false
        }
 
        subnet := (net.IPNet)(r.Subnet)
 
+       // Not the same address family
        if len(addr) != len(r.Subnet.IP) {
-               return fmt.Errorf("IP %s is not the same protocol as subnet %s",
-                       addr, subnet.String())
+               return false
        }
 
+       // Not in network
        if !subnet.Contains(addr) {
-               return fmt.Errorf("%s not in network %s", addr, subnet.String())
+               return false
        }
 
        // We ignore nils here so we can use this function as we initialize the range.
        if r.RangeStart != nil {
+               // Before the range start
                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)
+                       return false
                }
        }
 
        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)
+                       // After the  range end
+                       return false
                }
        }
 
-       return nil
+       return true
 }
 
 // Overlaps returns true if there is any overlap between ranges
@@ -127,10 +128,14 @@ func (r *Range) Overlaps(r1 *Range) bool {
                return false
        }
 
-       return r.IPInRange(r1.RangeStart) == nil ||
-               r.IPInRange(r1.RangeEnd) == nil ||
-               r1.IPInRange(r.RangeStart) == nil ||
-               r1.IPInRange(r.RangeEnd) == nil
+       return r.Contains(r1.RangeStart) ||
+               r.Contains(r1.RangeEnd) ||
+               r1.Contains(r.RangeStart) ||
+               r1.Contains(r.RangeEnd)
+}
+
+func (r *Range) String() string {
+       return fmt.Sprintf("%s-%s", r.RangeStart.String(), r.RangeEnd.String())
 }
 
 // canonicalizeIP makes sure a provided ip is in standard form
diff --git a/plugins/ipam/host-local/backend/allocator/range_set.go b/plugins/ipam/host-local/backend/allocator/range_set.go
new file mode 100644 (file)
index 0000000..efe2f94
--- /dev/null
@@ -0,0 +1,97 @@
+// 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"
+       "strings"
+)
+
+// Contains returns true if any range in this set contains an IP
+func (s *RangeSet) Contains(addr net.IP) bool {
+       r, _ := s.RangeFor(addr)
+       return r != nil
+}
+
+// RangeFor finds the range that contains an IP, or nil if not found
+func (s *RangeSet) RangeFor(addr net.IP) (*Range, error) {
+       if err := canonicalizeIP(&addr); err != nil {
+               return nil, err
+       }
+
+       for _, r := range *s {
+               if r.Contains(addr) {
+                       return &r, nil
+               }
+       }
+
+       return nil, fmt.Errorf("%s not in range set %s", addr.String(), s.String())
+}
+
+// Overlaps returns true if any ranges in any set overlap with this one
+func (s *RangeSet) Overlaps(p1 *RangeSet) bool {
+       for _, r := range *s {
+               for _, r1 := range *p1 {
+                       if r.Overlaps(&r1) {
+                               return true
+                       }
+               }
+       }
+       return false
+}
+
+// Canonicalize ensures the RangeSet is in a standard form, and detects any
+// invalid input. Call Range.Canonicalize() on every Range in the set
+func (s *RangeSet) Canonicalize() error {
+       if len(*s) == 0 {
+               return fmt.Errorf("empty range set")
+       }
+
+       fam := 0
+       for i, _ := range *s {
+               if err := (*s)[i].Canonicalize(); err != nil {
+                       return err
+               }
+               if i == 0 {
+                       fam = len((*s)[i].RangeStart)
+               } else {
+                       if fam != len((*s)[i].RangeStart) {
+                               return fmt.Errorf("mixed address families")
+                       }
+               }
+       }
+
+       // Make sure none of the ranges in the set overlap
+       l := len(*s)
+       for i, r1 := range (*s)[:l-1] {
+               for _, r2 := range (*s)[i+1:] {
+                       if r1.Overlaps(&r2) {
+                               return fmt.Errorf("subnets %s and %s overlap", r1.String(), r2.String())
+                       }
+               }
+       }
+
+       return nil
+}
+
+func (s *RangeSet) String() string {
+       out := []string{}
+       for _, r := range *s {
+               out = append(out, r.String())
+       }
+
+       return strings.Join(out, ",")
+}
diff --git a/plugins/ipam/host-local/backend/allocator/range_set_test.go b/plugins/ipam/host-local/backend/allocator/range_set_test.go
new file mode 100644 (file)
index 0000000..0aced38
--- /dev/null
@@ -0,0 +1,70 @@
+// 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/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("range sets", func() {
+       It("should detect set membership correctly", func() {
+               p := RangeSet{
+                       Range{Subnet: mustSubnet("192.168.0.0/24")},
+                       Range{Subnet: mustSubnet("172.16.1.0/24")},
+               }
+
+               err := p.Canonicalize()
+               Expect(err).NotTo(HaveOccurred())
+
+               Expect(p.Contains(net.IP{192, 168, 0, 55})).To(BeTrue())
+
+               r, err := p.RangeFor(net.IP{192, 168, 0, 55})
+               Expect(err).NotTo(HaveOccurred())
+               Expect(r).To(Equal(&p[0]))
+
+               r, err = p.RangeFor(net.IP{192, 168, 99, 99})
+               Expect(r).To(BeNil())
+               Expect(err).To(MatchError("192.168.99.99 not in range set 192.168.0.1-192.168.0.254,172.16.1.1-172.16.1.254"))
+
+       })
+
+       It("should discover overlaps within a set", func() {
+               p := RangeSet{
+                       {Subnet: mustSubnet("192.168.0.0/20")},
+                       {Subnet: mustSubnet("192.168.2.0/24")},
+               }
+
+               err := p.Canonicalize()
+               Expect(err).To(MatchError("subnets 192.168.0.1-192.168.15.254 and 192.168.2.1-192.168.2.254 overlap"))
+       })
+
+       It("should discover overlaps outside a set", func() {
+               p1 := RangeSet{
+                       {Subnet: mustSubnet("192.168.0.0/20")},
+               }
+               p2 := RangeSet{
+                       {Subnet: mustSubnet("192.168.2.0/24")},
+               }
+
+               p1.Canonicalize()
+               p2.Canonicalize()
+
+               Expect(p1.Overlaps(&p2)).To(BeTrue())
+               Expect(p2.Overlaps(&p1)).To(BeTrue())
+       })
+})
index 4b61ca5..cb8ca01 100644 (file)
@@ -77,11 +77,11 @@ var _ = Describe("IP ranges", func() {
        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"))
+               Expect(err).Should(MatchError("RangeStart 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"))
+               Expect(err).Should(MatchError("RangeEnd 192.0.4.0 not in network 192.0.2.0/24"))
 
                r = Range{
                        Subnet:     mustSubnet("192.0.2.0/24"),
@@ -89,7 +89,7 @@ var _ = Describe("IP ranges", func() {
                        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"))
+               Expect(err).Should(MatchError("RangeStart 192.0.2.50 not in network 192.0.2.0/24"))
        })
 
        It("should reject invalid gateways", func() {
@@ -126,15 +126,12 @@ var _ = Describe("IP ranges", func() {
                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.Contains(net.ParseIP("192.0.3.0"))).Should(BeFalse())
 
-               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"))
+               Expect(r.Contains(net.ParseIP("192.0.2.39"))).Should(BeFalse())
+               Expect(r.Contains(net.ParseIP("192.0.2.40"))).Should(BeTrue())
+               Expect(r.Contains(net.ParseIP("192.0.2.50"))).Should(BeTrue())
+               Expect(r.Contains(net.ParseIP("192.0.2.51"))).Should(BeFalse())
        })
 
        It("should accept v6 IPs in range and reject IPs out of range", func() {
@@ -145,15 +142,12 @@ var _ = Describe("IP ranges", func() {
                }
                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"))
+               Expect(r.Contains(net.ParseIP("2001:db8:2::"))).Should(BeFalse())
+
+               Expect(r.Contains(net.ParseIP("2001:db8:1::39"))).Should(BeFalse())
+               Expect(r.Contains(net.ParseIP("2001:db8:1::40"))).Should(BeTrue())
+               Expect(r.Contains(net.ParseIP("2001:db8:1::50"))).Should(BeTrue())
+               Expect(r.Contains(net.ParseIP("2001:db8:1::51"))).Should(BeFalse())
        })
 
        DescribeTable("Detecting overlap",
index 30b73c8..3e4b0d8 100644 (file)
@@ -45,17 +45,17 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "dataDir": "%s",
+"cniVersion": "0.3.1",
+"name": "mynet",
+"type": "ipvlan",
+"master": "foo0",
+       "ipam": {
+               "type": "host-local",
+               "dataDir": "%s",
                "resolvConf": "%s/resolv.conf",
                "ranges": [
-                       { "subnet": "10.1.2.0/24" },
-                       { "subnet": "2001:db8:1::0/64" }
+                       [{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}],
+                       [{ "subnet": "2001:db8:1::0/64" }]
                ],
                "routes": [
                        {"dst": "0.0.0.0/0"},
@@ -63,7 +63,7 @@ var _ = Describe("host-local Operations", func() {
                        {"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
                        {"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
                ]
-    }
+       }
 }`, tmpDir, tmpDir)
 
                args := &skel.CmdArgs{
@@ -117,12 +117,12 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
                Expect(string(contents)).To(Equal("dummy"))
 
-               lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
+               lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
                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==")
+               lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.1")
                contents, err = ioutil.ReadFile(lastFilePath2)
                Expect(err).NotTo(HaveOccurred())
                Expect(string(contents)).To(Equal("2001:db8:1::2"))
@@ -147,15 +147,15 @@ var _ = Describe("host-local Operations", func() {
                defer os.RemoveAll(tmpDir)
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.0",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "subnet": "10.1.2.0/24",
-        "dataDir": "%s"
-    }
+       "cniVersion": "0.3.0",
+       "name": "mynet",
+       "type": "ipvlan",
+       "master": "foo0",
+       "ipam": {
+               "type": "host-local",
+               "subnet": "10.1.2.0/24",
+               "dataDir": "%s"
+       }
 }`, tmpDir)
 
                args := &skel.CmdArgs{
@@ -184,16 +184,16 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.1.0",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "subnet": "10.1.2.0/24",
-        "dataDir": "%s",
-       "resolvConf": "%s/resolv.conf"
-    }
+       "cniVersion": "0.1.0",
+       "name": "mynet",
+       "type": "ipvlan",
+       "master": "foo0",
+       "ipam": {
+               "type": "host-local",
+               "subnet": "10.1.2.0/24",
+               "dataDir": "%s",
+               "resolvConf": "%s/resolv.conf"
+       }
 }`, tmpDir, tmpDir)
 
                args := &skel.CmdArgs{
@@ -224,7 +224,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.CgECAQ==")
+               lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
                contents, err = ioutil.ReadFile(lastFilePath)
                Expect(err).NotTo(HaveOccurred())
                Expect(string(contents)).To(Equal("10.1.2.2"))
@@ -250,15 +250,15 @@ var _ = Describe("host-local Operations", func() {
                defer os.RemoveAll(tmpDir)
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "subnet": "10.1.2.0/24",
-        "dataDir": "%s"
-    }
+       "cniVersion": "0.3.1",
+       "name": "mynet",
+       "type": "ipvlan",
+       "master": "foo0",
+       "ipam": {
+               "type": "host-local",
+               "subnet": "10.1.2.0/24",
+               "dataDir": "%s"
+       }
 }`, tmpDir)
 
                args := &skel.CmdArgs{
@@ -301,15 +301,15 @@ var _ = Describe("host-local Operations", func() {
                defer os.RemoveAll(tmpDir)
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.2.0",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "subnet": "10.1.2.0/24",
-        "dataDir": "%s"
-    }
+       "cniVersion": "0.2.0",
+       "name": "mynet",
+       "type": "ipvlan",
+       "master": "foo0",
+       "ipam": {
+               "type": "host-local",
+               "subnet": "10.1.2.0/24",
+               "dataDir": "%s"
+       }
 }`, tmpDir)
 
                args := &skel.CmdArgs{
@@ -336,17 +336,17 @@ var _ = Describe("host-local Operations", func() {
                defer os.RemoveAll(tmpDir)
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "dataDir": "%s",
+       "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.2.0/24" }]
                ]
-    },
+       },
        "args": {
                "cni": {
                        "ips": ["10.1.2.88"]
@@ -384,18 +384,18 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "dataDir": "%s",
+       "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" }
+                       [{ "subnet": "10.1.2.0/24" }],
+                       [{ "subnet": "10.1.3.0/24" }]
                ]
-    },
+       },
        "args": {
                "cni": {
                        "ips": ["10.1.2.88", "10.1.3.77"]
@@ -434,18 +434,18 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "dataDir": "%s",
+       "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" }
+                       [{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
+                       [{ "subnet": "2001:db8:1::/24" }]
                ]
-    },
+       },
        "args": {
                "cni": {
                        "ips": ["10.1.2.88", "2001:db8:1::999"]
@@ -481,18 +481,18 @@ var _ = Describe("host-local Operations", func() {
                defer os.RemoveAll(tmpDir)
 
                conf := fmt.Sprintf(`{
-    "cniVersion": "0.3.1",
-    "name": "mynet",
-    "type": "ipvlan",
-    "master": "foo0",
-    "ipam": {
-        "type": "host-local",
-        "dataDir": "%s",
+       "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" }
+                       [{ "subnet": "10.1.2.0/24" }],
+                       [{ "subnet": "10.1.3.0/24" }]
                ]
-    },
+       },
        "args": {
                "cni": {
                        "ips": ["10.1.2.88", "10.1.2.77"]
index e554a70..9e2bacc 100644 (file)
@@ -66,13 +66,13 @@ func cmdAdd(args *skel.CmdArgs) error {
                requestedIPs[ip.String()] = ip
        }
 
-       for idx, ipRange := range ipamConf.Ranges {
-               allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
+       for idx, rangeset := range ipamConf.Ranges {
+               allocator := allocator.NewIPAllocator(&rangeset, store, idx)
 
                // 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 {
+                       if rangeset.Contains(ip) {
                                requestedIP = ip
                                delete(requestedIPs, k)
                                break
@@ -124,8 +124,8 @@ func cmdDel(args *skel.CmdArgs) error {
 
        // 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)
+       for idx, rangeset := range ipamConf.Ranges {
+               ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
 
                err := ipAllocator.Release(args.ContainerID)
                if err != nil {
index 2939b12..13e7257 100644 (file)
@@ -94,14 +94,14 @@ const (
        rangesStartStr = `,
         "ranges": [`
        rangeSubnetConfStr = `
-            {
+            [{
                 "subnet":  "%s"
-            }`
+            }]`
        rangeSubnetGWConfStr = `
-            {
+            [{
                 "subnet":  "%s",
                 "gateway": "%s"
-            }`
+            }]`
        rangesEndStr = `
         ]`
 
index ac77d63..e68c630 100644 (file)
@@ -155,8 +155,8 @@ var _ = Describe("ptp Operations", func() {
     "ipam": {
         "type": "host-local",
                "ranges": [
-                       { "subnet": "10.1.2.0/24"},
-                       { "subnet": "2001:db8:1::0/66"}
+                       [{ "subnet": "10.1.2.0/24"}],
+                       [{ "subnet": "2001:db8:1::0/66"}]
                ]
     }
 }`