ipam/host-local: support multiple IP ranges
authorCasey Callendrello <casey.callendrello@coreos.com>
Thu, 11 May 2017 15:01:20 +0000 (17:01 +0200)
committerCasey Callendrello <casey.callendrello@coreos.com>
Mon, 12 Jun 2017 19:14:44 +0000 (21:14 +0200)
This change allows the host-local allocator to allocate multiple IPs.
This is intended to enable dual-stack, but is not limited to only two
subnets or separate address families.

13 files changed:
pkg/ip/cidr.go
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 [new file with mode: 0644]
plugins/ipam/host-local/backend/allocator/range.go [new file with mode: 0644]
plugins/ipam/host-local/backend/allocator/range_test.go [new file with mode: 0644]
plugins/ipam/host-local/backend/disk/backend.go
plugins/ipam/host-local/backend/store.go
plugins/ipam/host-local/backend/testing/fake_store.go
plugins/ipam/host-local/host_local_test.go
plugins/ipam/host-local/main.go

index dae2c4d..7acc2d4 100644 (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)
index 073e369..7ce4a68 100644 (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.
index 56b8bdf..d4c2797 100644 (file)
 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
-       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)
-       }
-
-       var (
-               start net.IP
-               end   net.IP
-               err   error
-       )
-       start, end, err = networkRange((*net.IPNet)(&conf.Subnet))
-       if err != nil {
-               return nil, err
-       }
-
-       // 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
+       netName string
+       ipRange Range
+       store   backend.Store
+       rangeID string // Used for tracking last reserved ip
 }
 
-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)
+type RangeIter struct {
+       low   net.IP
+       high  net.IP
+       cur   net.IP
+       start net.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
-       }
+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)
 
-       if !ipnet.Contains(ip) {
-               return fmt.Errorf("%s not in network: %s", ip, ipnet)
+       return &IPAllocator{
+               netName: netName,
+               ipRange: r,
+               store:   store,
+               rangeID: rangeID,
        }
-
-       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
                }
+               for {
+                       cur := iter.Next()
+                       if cur == nil {
+                               break
+                       }
 
-               if reserved {
-                       ipConfig := &current.IPConfig{
-                               Version: "4",
-                               Address: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
-                               Gateway: gw,
+                       // don't allocate gateway IP
+                       if gw != nil && cur.Equal(gw) {
+                               continue
                        }
-                       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, a.rangeID)
+                       if err != nil {
+                               return nil, err
+                       }
 
-               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,
+                       if reserved {
+                               reservedIP = cur
+                               break
                        }
-                       routes := convertRoutesToCurrent(a.conf.Routes)
-                       return ipConfig, routes, nil
-               }
-               // break here to complete the loop
-               if cur.Equal(endIP) {
-                       break
                }
        }
-       return nil, nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
+
+       if reservedIP == nil {
+               return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
+       }
+       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")
-       }
-
-       if len(ip) != len(ipnet.Mask) {
-               return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch")
+// 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,
        }
 
-       var end net.IP
-       for i := 0; i < len(ip); i++ {
-               end = append(end, ip[i]|^ipnet.Mask[i])
-       }
+       // Round-robin by trying to allocate from the last reserved IP + 1
+       startFromLastReservedIP := false
 
-       // Exclude the broadcast address for IPv4
-       if ip.To4() != nil {
-               end[3]--
+       // 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 retrieving last reserved ip: %v", err)
+       } else if lastReservedIP != nil {
+               startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
        }
 
-       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
+       if startFromLastReservedIP {
+               if i.high.Equal(lastReservedIP) {
+                       i.start = i.low
+               } else {
+                       i.start = ip.NextIP(lastReservedIP)
+               }
+       } else {
+               i.start = a.ipRange.RangeStart
        }
-       return ip.NextIP(curIP)
+       return &i, nil
 }
 
-// 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
-       startFromLastReservedIP := false
-       lastReservedIP, err := a.store.LastReservedIP()
-       if err != nil && !os.IsNotExist(err) {
-               log.Printf("Error retriving 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
-               }
+// 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
        }
-       if startFromLastReservedIP {
-               startIP = a.nextIP(lastReservedIP)
-               endIP = lastReservedIP
+       //  we returned .high last time, since we're inclusive
+       if i.cur.Equal(i.high) {
+               i.cur = i.low
        } else {
-               startIP = a.start
-               endIP = a.end
+               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 startIP, endIP
+
+       return i.cur
 }
index 54ead09..2c258a9 100644 (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
+       conf := Range{
+               Subnet: types.IPNet(*subnet),
        }
-       res, routes, err := alloc.Get("ID")
-       if err != nil {
-               return nil, nil, err
+
+       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 res, routes, nil
+       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 RangeStart first", func() {
-                       subnet, err := types.ParseCIDR("192.168.1.0/24")
+               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"))
 
-                       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)
+                       err = alloc.Release("ID")
                        Expect(err).ToNot(HaveOccurred())
 
-                       res, _, err := alloc.Get("ID")
+                       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() {
+                       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")
+                               alloc := mkalloc()
+                               requestedIP := net.IP{192, 168, 1, 5}
+                               res, err := alloc.Get("ID", requestedIP)
                                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")
+                               Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
+                       })
+
+                       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())
                                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() {
-                               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.50")},
-                                       RangeEnd: net.ParseIP("192.168.1.20"),
-                               }
-                               store := fakestore.NewFakeStore(ipmap, nil)
-                               alloc, _ := NewIPAllocator(&conf, store)
-                               _, _, err = alloc.Get("ID")
+                               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"))
-                       }
-               })
-       })
-
-       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},
+                       for idx, tc := range testCases {
+                               _, err := tc.run(idx)
+                               Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet))
                        }
-                       store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
-                       _, err = NewIPAllocator(&conf, store)
-                       Expect(err).To(HaveOccurred())
                })
        })
 })
index 8c00448..19e2f3c 100644 (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
-}
diff --git a/plugins/ipam/host-local/backend/allocator/config_test.go b/plugins/ipam/host-local/backend/allocator/config_test.go
new file mode 100644 (file)
index 0000000..b162ab4
--- /dev/null
@@ -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())
+       })
+})
diff --git a/plugins/ipam/host-local/backend/allocator/range.go b/plugins/ipam/host-local/backend/allocator/range.go
new file mode 100644 (file)
index 0000000..5c7a1dd
--- /dev/null
@@ -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
+}
diff --git a/plugins/ipam/host-local/backend/allocator/range_test.go b/plugins/ipam/host-local/backend/allocator/range_test.go
new file mode 100644 (file)
index 0000000..4b61ca5
--- /dev/null
@@ -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)
+}
index 483103a..82c3a8a 100644 (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
index 82ba869..3d69584 100644 (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
 }
index c35a130..49a0f55 100644 (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
+}
index 5e2c69c..68942ea 100644 (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")
+               // 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"),
+                       }))
+
+               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(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")))
+               Expect(string(contents)).To(Equal("dummy"))
 
-               ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
-               contents, err := ioutil.ReadFile(ipFilePath)
+               ipFilePath2 := filepath.Join(tmpDir, "mynet", "2001:db8:1::2")
+               contents, err = ioutil.ReadFile(ipFilePath2)
                Expect(err).NotTo(HaveOccurred())
                Expect(string(contents)).To(Equal("dummy"))
 
-               lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
-               contents, err = ioutil.ReadFile(lastFilePath)
+               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
+}
index 4db9a07..e554a70 100644 (file)
 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)
+       }
+
+       // 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.IPs = []*current.IPConfig{ipConf}
-       result.Routes = routes
+
+       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
 }