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.
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)
## 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": {
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.
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 := ¤t.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 := ¤t.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 ¤t.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()
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
}
-// 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.
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 {
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{
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{},
},
}
- 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{
},
},
}
- 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())
})
})
})
"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
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
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
-}
--- /dev/null
+// 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())
+ })
+})
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+}
"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
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
}
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) {
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
}
// 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
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
}
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 {
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 {
}
return nil
}
+
+func (s *FakeStore) SetIPMap(m map[string]string) {
+ s.ipMap = m
+}
)
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"
"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)
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())
})
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"))
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
+}
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"
}
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)
}
}
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
}