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

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

View File

@ -31,6 +31,16 @@ func PrevIP(ip net.IP) net.IP {
return intToIP(i.Sub(i, big.NewInt(1))) 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 { func ipToInt(ip net.IP) *big.Int {
if v := ip.To4(); v != nil { if v := ip.To4(); v != nil {
return big.NewInt(0).SetBytes(v) return big.NewInt(0).SetBytes(v)

View File

@ -5,30 +5,41 @@ it can include a DNS configuration from a `resolv.conf` file on the host.
## Overview ## 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. It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
## Example configurations ## Example configurations
IPv4:
```json ```json
{ {
"ipam": { "ipam": {
"type": "host-local", "type": "host-local",
"ranges": [
{
"subnet": "10.10.0.0/16", "subnet": "10.10.0.0/16",
"rangeStart": "10.10.1.20", "rangeStart": "10.10.1.20",
"rangeEnd": "10.10.3.50", "rangeEnd": "10.10.3.50",
"gateway": "10.10.0.254", "gateway": "10.10.0.254"
},
{
"subnet": "3ffe:ffff:0:01ff::/64",
"rangeStart": "3ffe:ffff:0:01ff::0010",
"rangeEnd": "3ffe:ffff:0:01ff::0020"
}
],
"routes": [ "routes": [
{ "dst": "0.0.0.0/0" }, { "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 ```json
{ {
"ipam": { "ipam": {
@ -47,36 +58,65 @@ IPv6:
We can test it out on the command-line: We can test it out on the command-line:
```bash ```bash
$ export CNI_COMMAND=ADD $ 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
$ export CNI_CONTAINERID=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
$ echo '{ "name": "default", "ipam": { "type": "host-local", "subnet": "203.0.113.0/24" } }' | ./host-local
``` ```
```json ```json
{ {
"ip4": { "ips": [
"ip": "203.0.113.1/24" {
"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 ## Network configuration reference
* `type` (string, required): "host-local". * `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. * `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 * `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 * `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 ## Supported arguments
The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported: 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 ## Files
Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`. The prefix can be customized with the `dataDir` option listed above. Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`.
The path can be customized with the `dataDir` option listed above. Environments
where IPs are released automatically on reboot (e.g. running containers are not
restored) may wish to specify `/var/run/cni` or another tmpfs mounted directory
instead.

View File

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

View File

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

View File

@ -20,35 +20,51 @@ import (
"net" "net"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
types020 "github.com/containernetworking/cni/pkg/types/020"
) )
// IPAMConfig represents the IP related network configuration. // 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 { type IPAMConfig struct {
*Range
Name string Name string
Type string `json:"type"` Type string `json:"type"`
RangeStart net.IP `json:"rangeStart"` Routes []*types.Route `json:"routes"`
RangeEnd net.IP `json:"rangeEnd"`
Subnet types.IPNet `json:"subnet"`
Gateway net.IP `json:"gateway"`
Routes []types.Route `json:"routes"`
DataDir string `json:"dataDir"` DataDir string `json:"dataDir"`
ResolvConf string `json:"resolvConf"` ResolvConf string `json:"resolvConf"`
Args *IPAMArgs `json:"-"` Ranges []Range `json:"ranges"`
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
} }
type IPAMArgs struct { type IPAMEnvArgs struct {
types.CommonArgs types.CommonArgs
IP net.IP `json:"ip,omitempty"` 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 { type Net struct {
Name string `json:"name"` Name string `json:"name"`
CNIVersion string `json:"cniVersion"` CNIVersion string `json:"cniVersion"`
IPAM *IPAMConfig `json:"ipam"` 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. // 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{} n := Net{}
if err := json.Unmarshal(bytes, &n); err != nil { if err := json.Unmarshal(bytes, &n); err != nil {
return nil, "", err 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") return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
} }
if args != "" { // Parse custom IP from both env args *and* the top-level args config
n.IPAM.Args = &IPAMArgs{} if envArgs != "" {
err := types.LoadArgs(args, n.IPAM.Args) e := IPAMEnvArgs{}
err := types.LoadArgs(envArgs, &e)
if err != nil { if err != nil {
return nil, "", err 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 // 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 return n.IPAM, n.CNIVersion, nil
} }
func convertRoutesToCurrent(routes []types.Route) []*types.Route {
var currentRoutes []*types.Route
for _, r := range routes {
currentRoutes = append(currentRoutes, &types.Route{
Dst: r.Dst,
GW: r.GW,
})
}
return currentRoutes
}

View File

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

View File

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

View File

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

View File

@ -24,10 +24,12 @@ import (
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend" "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" 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 { type Store struct {
FileLock FileLock
dataDir string dataDir string
@ -41,7 +43,7 @@ func New(network, dataDir string) (*Store, error) {
dataDir = defaultDataDir dataDir = defaultDataDir
} }
dir := filepath.Join(dataDir, network) 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 nil, err
} }
@ -52,7 +54,7 @@ func New(network, dataDir string) (*Store, error) {
return &Store{*lk, dir}, nil 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()) fname := filepath.Join(s.dataDir, ip.String())
f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644) f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
if os.IsExist(err) { if os.IsExist(err) {
@ -71,7 +73,7 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
return false, err return false, err
} }
// store the reserved ip in lastIPFile // 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) err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
if err != nil { if err != nil {
return false, err 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 // LastReservedIP returns the last reserved IP if exists
func (s *Store) LastReservedIP() (net.IP, error) { func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
ipfile := filepath.Join(s.dataDir, lastIPFile) ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
data, err := ioutil.ReadFile(ipfile) data, err := ioutil.ReadFile(ipfile)
if err != nil { if err != nil {
return nil, err return nil, err

View File

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

View File

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

View File

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

View File

@ -15,6 +15,10 @@
package main package main
import ( import (
"fmt"
"net"
"strings"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk" "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
@ -50,17 +54,58 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
defer store.Close() defer store.Close()
allocator, err := allocator.NewIPAllocator(ipamConf, store) // Keep the allocators we used, so we can release all IPs if an error
if err != nil { // occurs after we start allocating
return err 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) for idx, ipRange := range ipamConf.Ranges {
if err != nil { allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
return err
// 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
} }
result.IPs = []*current.IPConfig{ipConf} }
result.Routes = routes
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.Routes = ipamConf.Routes
return types.PrintResult(result, confVersion) return types.PrintResult(result, confVersion)
} }
@ -77,10 +122,19 @@ func cmdDel(args *skel.CmdArgs) error {
} }
defer store.Close() defer store.Close()
ipAllocator, err := allocator.NewIPAllocator(ipamConf, store) // 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 { if err != nil {
return err errors = append(errors, err.Error())
}
} }
return ipAllocator.Release(args.ContainerID) if errors != nil {
return fmt.Errorf(strings.Join(errors, ";"))
}
return nil
} }