In real-world address allocations, disjoint address ranges are common.
Therefore, the host-local allocator should support them.
This change still allows for multiple IPs in a single configuration, but
also allows for a "set of subnets."
Fixes: #45
host-local IPAM plugin allocates ip addresses out of a set of address ranges.
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
+The allocator can allocate multiple ranges, and supports sets of multiple (disjoint)
+subnets. The allocation strategy is loosely round-robin within each range set.
+
## Example configurations
+Note that the key `ranges` is a list of range sets. That is to say, the length
+of the top-level array is the number of addresses returned. The second-level
+array is a set of subnets to use as a pool of possible addresses.
+
+This example configuration returns 2 IP addresses.
+
```json
{
"ipam": {
"type": "host-local",
"ranges": [
- {
- "subnet": "10.10.0.0/16",
- "rangeStart": "10.10.1.20",
- "rangeEnd": "10.10.3.50",
- "gateway": "10.10.0.254"
- },
- {
- "subnet": "3ffe:ffff:0:01ff::/64",
- "rangeStart": "3ffe:ffff:0:01ff::0010",
- "rangeEnd": "3ffe:ffff:0:01ff::0020"
- }
+ [
+ {
+ "subnet": "10.10.0.0/16",
+ "rangeStart": "10.10.1.20",
+ "rangeEnd": "10.10.3.50",
+ "gateway": "10.10.0.254"
+ },
+ {
+ "subnet": "172.16.5.0/24"
+ }
+ ],
+ [
+ {
+ "subnet": "3ffe:ffff:0:01ff::/64",
+ "rangeStart": "3ffe:ffff:0:01ff::0010",
+ "rangeEnd": "3ffe:ffff:0:01ff::0020"
+ }
+ ]
],
"routes": [
{ "dst": "0.0.0.0/0" },
We can test it out on the command-line:
```bash
-$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ {"subnet": "203.0.113.0/24"}, {"subnet": "2001:db8:1::/64"}], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
+$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ [{"subnet": "203.0.113.0/24"}], [{"subnet": "2001:db8:1::/64"}]], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
```
* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
-* `ranges`, (array, required, nonempty) an array of range objects:
+* `ranges`, (array, required, nonempty) an array of arrays of range objects:
* `subnet` (string, required): CIDR block to allocate out of.
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
* `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6
package allocator
import (
- "encoding/base64"
"fmt"
"log"
"net"
"os"
+ "strconv"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ip"
)
type IPAllocator struct {
- netName string
- ipRange Range
- store backend.Store
- rangeID string // Used for tracking last reserved ip
+ rangeset *RangeSet
+ store backend.Store
+ rangeID string // Used for tracking last reserved ip
}
-type RangeIter struct {
- low net.IP
- high net.IP
- cur net.IP
- start net.IP
-}
-
-func NewIPAllocator(netName string, r Range, store backend.Store) *IPAllocator {
- // The range name (last allocated ip suffix) is just the base64
- // encoding of the bytes of the first IP
- rangeID := base64.URLEncoding.EncodeToString(r.RangeStart)
-
+func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator {
return &IPAllocator{
- netName: netName,
- ipRange: r,
- store: store,
- rangeID: rangeID,
+ rangeset: s,
+ store: store,
+ rangeID: strconv.Itoa(id),
}
}
a.store.Lock()
defer a.store.Unlock()
- gw := a.ipRange.Gateway
-
- var reservedIP net.IP
+ var reservedIP *net.IPNet
+ var gw net.IP
if requestedIP != nil {
- if gw != nil && gw.Equal(requestedIP) {
- return nil, fmt.Errorf("requested IP must differ from gateway IP")
+ if err := canonicalizeIP(&requestedIP); err != nil {
+ return nil, err
}
- if err := a.ipRange.IPInRange(requestedIP); err != nil {
+ r, err := a.rangeset.RangeFor(requestedIP)
+ if err != nil {
return nil, err
}
+ if requestedIP.Equal(r.Gateway) {
+ return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
+ }
+
reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
if err != nil {
return nil, err
}
if !reserved {
- return nil, fmt.Errorf("requested IP address %q is not available in network: %s %s", requestedIP, a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
+ return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String())
}
- reservedIP = requestedIP
+ reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask}
+ gw = r.Gateway
} else {
iter, err := a.GetIter()
return nil, err
}
for {
- cur := iter.Next()
- if cur == nil {
+ reservedIP, gw = iter.Next()
+ if reservedIP == nil {
break
}
- // don't allocate gateway IP
- if gw != nil && cur.Equal(gw) {
- continue
- }
-
- reserved, err := a.store.Reserve(id, cur, a.rangeID)
+ reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID)
if err != nil {
return nil, err
}
if reserved {
- reservedIP = cur
break
}
}
}
if reservedIP == nil {
- return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
+ return nil, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String())
}
version := "4"
- if reservedIP.To4() == nil {
+ if reservedIP.IP.To4() == nil {
version = "6"
}
return ¤t.IPConfig{
Version: version,
- Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask},
+ Address: *reservedIP,
Gateway: gw,
}, nil
}
return a.store.ReleaseByID(id)
}
+type RangeIter struct {
+ rangeset *RangeSet
+
+ // The current range id
+ rangeIdx int
+
+ // Our current position
+ cur net.IP
+
+ // The IP and range index where we started iterating; if we hit this again, we're done.
+ startIP net.IP
+ startRange int
+}
+
// GetIter encapsulates the strategy for this allocator.
-// We use a round-robin strategy, attempting to evenly use the whole subnet.
+// We use a round-robin strategy, attempting to evenly use the whole set.
// More specifically, a crash-looping container will not see the same IP until
// the entire range has been run through.
// We may wish to consider avoiding recently-released IPs in the future.
func (a *IPAllocator) GetIter() (*RangeIter, error) {
- i := RangeIter{
- low: a.ipRange.RangeStart,
- high: a.ipRange.RangeEnd,
+ iter := RangeIter{
+ rangeset: a.rangeset,
}
// Round-robin by trying to allocate from the last reserved IP + 1
if err != nil && !os.IsNotExist(err) {
log.Printf("Error retrieving last reserved ip: %v", err)
} else if lastReservedIP != nil {
- startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
+ startFromLastReservedIP = a.rangeset.Contains(lastReservedIP)
}
+ // Find the range in the set with this IP
if startFromLastReservedIP {
- if i.high.Equal(lastReservedIP) {
- i.start = i.low
- } else {
- i.start = ip.NextIP(lastReservedIP)
+ for i, r := range *a.rangeset {
+ if r.Contains(lastReservedIP) {
+ iter.rangeIdx = i
+ iter.startRange = i
+
+ // We advance the cursor on every Next(), so the first call
+ // to next() will return lastReservedIP + 1
+ iter.cur = lastReservedIP
+ break
+ }
}
} else {
- i.start = a.ipRange.RangeStart
+ iter.rangeIdx = 0
+ iter.startRange = 0
+ iter.startIP = (*a.rangeset)[0].RangeStart
}
- return &i, nil
+ return &iter, nil
}
-// Next returns the next IP in the iterator, or nil if end is reached
-func (i *RangeIter) Next() net.IP {
- // If we're at the beginning, time to start
+// Next returns the next IP, its mask, and its gateway. Returns nil
+// if the iterator has been exhausted
+func (i *RangeIter) Next() (*net.IPNet, net.IP) {
+ r := (*i.rangeset)[i.rangeIdx]
+
+ // If this is the first time iterating and we're not starting in the middle
+ // of the range, then start at rangeStart, which is inclusive
if i.cur == nil {
- i.cur = i.start
- return i.cur
+ i.cur = r.RangeStart
+ i.startIP = i.cur
+ if i.cur.Equal(r.Gateway) {
+ return i.Next()
+ }
+ return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
}
- // we returned .high last time, since we're inclusive
- if i.cur.Equal(i.high) {
- i.cur = i.low
+
+ // If we've reached the end of this range, we need to advance the range
+ // RangeEnd is inclusive as well
+ if i.cur.Equal(r.RangeEnd) {
+ i.rangeIdx += 1
+ i.rangeIdx %= len(*i.rangeset)
+ r = (*i.rangeset)[i.rangeIdx]
+
+ i.cur = r.RangeStart
} else {
i.cur = ip.NextIP(i.cur)
}
- // If we've looped back to where we started, exit
- if i.cur.Equal(i.start) {
- return nil
+ if i.startIP == nil {
+ i.startIP = i.cur
+ } else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) {
+ // IF we've looped back to where we started, give up
+ return nil, nil
+ }
+
+ if i.cur.Equal(r.Gateway) {
+ return i.Next()
}
- return i.cur
+ return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
}
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
fakestore "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/testing"
+
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
type AllocatorTestCase struct {
- subnet string
+ subnets []string
ipmap map[string]string
expectResult string
lastIP string
}
func mkalloc() IPAllocator {
- ipnet, _ := types.ParseCIDR("192.168.1.0/24")
-
- r := Range{
- Subnet: types.IPNet(*ipnet),
+ p := RangeSet{
+ Range{Subnet: mustSubnet("192.168.1.0/29")},
}
- r.Canonicalize()
+ p.Canonicalize()
store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{})
alloc := IPAllocator{
- netName: "netname",
- ipRange: r,
- store: store,
- rangeID: "rangeid",
+ rangeset: &p,
+ store: store,
+ rangeID: "rangeid",
}
return alloc
func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
fmt.Fprintln(GinkgoWriter, "Index:", idx)
- subnet, err := types.ParseCIDR(t.subnet)
- if err != nil {
- return nil, err
- }
-
- conf := Range{
- Subnet: types.IPNet(*subnet),
+ p := RangeSet{}
+ for _, s := range t.subnets {
+ subnet, err := types.ParseCIDR(s)
+ if err != nil {
+ return nil, err
+ }
+ p = append(p, Range{Subnet: types.IPNet(*subnet)})
}
- Expect(conf.Canonicalize()).To(BeNil())
+ Expect(p.Canonicalize()).To(BeNil())
store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)})
alloc := IPAllocator{
- "netname",
- conf,
- store,
- "rangeid",
+ rangeset: &p,
+ store: store,
+ rangeID: "rangeid",
}
return alloc.Get("ID", nil)
var _ = Describe("host-local ip allocator", func() {
Context("RangeIter", func() {
It("should loop correctly from the beginning", func() {
- r := RangeIter{
- start: net.IP{10, 0, 0, 0},
- low: net.IP{10, 0, 0, 0},
- high: net.IP{10, 0, 0, 5},
- }
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
- Expect(r.Next()).To(BeNil())
+ a := mkalloc()
+ r, _ := a.GetIter()
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+ Expect(r.nextip()).To(BeNil())
})
It("should loop correctly from the end", func() {
- r := RangeIter{
- start: net.IP{10, 0, 0, 5},
- low: net.IP{10, 0, 0, 0},
- high: net.IP{10, 0, 0, 5},
- }
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
- Expect(r.Next()).To(BeNil())
+ a := mkalloc()
+ a.store.Reserve("ID", net.IP{192, 168, 1, 6}, a.rangeID)
+ a.store.ReleaseByID("ID")
+ r, _ := a.GetIter()
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+ Expect(r.nextip()).To(BeNil())
})
-
It("should loop correctly from the middle", func() {
- r := RangeIter{
- start: net.IP{10, 0, 0, 3},
- low: net.IP{10, 0, 0, 0},
- high: net.IP{10, 0, 0, 5},
- }
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
- Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
- Expect(r.Next()).To(BeNil())
+ a := mkalloc()
+ a.store.Reserve("ID", net.IP{192, 168, 1, 3}, a.rangeID)
+ a.store.ReleaseByID("ID")
+ r, _ := a.GetIter()
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2}))
+ Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3}))
+ Expect(r.nextip()).To(BeNil())
})
-
})
Context("when has free ip", func() {
testCases := []AllocatorTestCase{
// fresh start
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{},
expectResult: "10.0.0.2",
lastIP: "",
},
{
- subnet: "2001:db8:1::0/64",
+ subnets: []string{"2001:db8:1::0/64"},
ipmap: map[string]string{},
expectResult: "2001:db8:1::2",
lastIP: "",
},
{
- subnet: "10.0.0.0/30",
+ subnets: []string{"10.0.0.0/30"},
ipmap: map[string]string{},
expectResult: "10.0.0.2",
lastIP: "",
},
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.2": "id",
},
},
// next ip of last reserved ip
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{},
expectResult: "10.0.0.6",
lastIP: "10.0.0.5",
},
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.4": "id",
"10.0.0.5": "id",
},
// round robin to the beginning
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.6": "id",
},
},
// lastIP is out of range
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.2": "id",
},
expectResult: "10.0.0.3",
lastIP: "10.0.0.128",
},
+ // subnet is completely full except for lastip
// wrap around and reserve lastIP
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.2": "id",
"10.0.0.4": "id",
expectResult: "10.0.0.3",
lastIP: "10.0.0.3",
},
+ // alocate from multiple subnets
+ {
+ subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
+ expectResult: "10.0.0.2",
+ ipmap: map[string]string{},
+ },
+ // advance to next subnet
+ {
+ subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
+ lastIP: "10.0.0.2",
+ expectResult: "10.0.1.2",
+ ipmap: map[string]string{},
+ },
+ // Roll to start subnet
+ {
+ subnets: []string{"10.0.0.0/30", "10.0.1.0/30", "10.0.2.0/30"},
+ lastIP: "10.0.2.2",
+ expectResult: "10.0.0.2",
+ ipmap: map[string]string{},
+ },
}
for idx, tc := range testCases {
It("should not allocate the broadcast address", func() {
alloc := mkalloc()
- for i := 2; i < 255; i++ {
+ for i := 2; i < 7; i++ {
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
- s := fmt.Sprintf("192.168.1.%d/24", i)
+ s := fmt.Sprintf("192.168.1.%d/29", i)
Expect(s).To(Equal(res.Address.String()))
fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String())
}
alloc := mkalloc()
res, err := alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
- Expect(res.Address.String()).To(Equal("192.168.1.2/24"))
+ Expect(res.Address.String()).To(Equal("192.168.1.2/29"))
err = alloc.Release("ID")
Expect(err).ToNot(HaveOccurred())
res, err = alloc.Get("ID", nil)
Expect(err).ToNot(HaveOccurred())
- Expect(res.Address.String()).To(Equal("192.168.1.3/24"))
+ Expect(res.Address.String()).To(Equal("192.168.1.3/29"))
})
- It("should allocate RangeStart first", func() {
- alloc := mkalloc()
- alloc.ipRange.RangeStart = net.IP{192, 168, 1, 10}
- res, err := alloc.Get("ID", nil)
- Expect(err).ToNot(HaveOccurred())
- Expect(res.Address.String()).To(Equal("192.168.1.10/24"))
-
- res, err = alloc.Get("ID", nil)
- Expect(err).ToNot(HaveOccurred())
- Expect(res.Address.String()).To(Equal("192.168.1.11/24"))
- })
-
- It("should allocate RangeEnd but not past RangeEnd", func() {
- alloc := mkalloc()
- alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
-
- for i := 1; i < 5; i++ {
- res, err := alloc.Get("ID", nil)
- Expect(err).ToNot(HaveOccurred())
- // i+1 because the gateway address is skipped
- Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1)))
- }
-
- _, err := alloc.Get("ID", nil)
- Expect(err).To(HaveOccurred())
- })
-
Context("when requesting a specific IP", func() {
It("must allocate the requested IP", func() {
alloc := mkalloc()
Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
_, err = alloc.Get("ID", requestedIP)
- Expect(err).To(MatchError(`requested IP address "192.168.1.5" is not available in network: netname 192.168.1.0/24`))
+ Expect(err).To(MatchError(`requested IP address 192.168.1.5 is not available in range set 192.168.1.1-192.168.1.6`))
})
It("must return an error when the requested IP is after RangeEnd", func() {
alloc := mkalloc()
- alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
- requestedIP := net.IP{192, 168, 1, 6}
+ (*alloc.rangeset)[0].RangeEnd = net.IP{192, 168, 1, 4}
+ requestedIP := net.IP{192, 168, 1, 5}
_, err := alloc.Get("ID", requestedIP)
Expect(err).To(HaveOccurred())
})
It("must return an error when the requested IP is before RangeStart", func() {
alloc := mkalloc()
- alloc.ipRange.RangeStart = net.IP{192, 168, 1, 6}
- requestedIP := net.IP{192, 168, 1, 5}
+ (*alloc.rangeset)[0].RangeStart = net.IP{192, 168, 1, 3}
+ requestedIP := net.IP{192, 168, 1, 2}
_, err := alloc.Get("ID", requestedIP)
Expect(err).To(HaveOccurred())
})
It("returns a meaningful error", func() {
testCases := []AllocatorTestCase{
{
- subnet: "10.0.0.0/30",
+ subnets: []string{"10.0.0.0/30"},
ipmap: map[string]string{
"10.0.0.2": "id",
- "10.0.0.3": "id",
},
},
{
- subnet: "10.0.0.0/29",
+ subnets: []string{"10.0.0.0/29"},
ipmap: map[string]string{
"10.0.0.2": "id",
"10.0.0.3": "id",
"10.0.0.4": "id",
"10.0.0.5": "id",
"10.0.0.6": "id",
- "10.0.0.7": "id",
+ },
+ },
+ {
+ subnets: []string{"10.0.0.0/30", "10.0.1.0/30"},
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ "10.0.1.2": "id",
},
},
}
for idx, tc := range testCases {
_, err := tc.run(idx)
- Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet))
+ Expect(err).NotTo(BeNil())
+ Expect(err.Error()).To(HavePrefix("no IP addresses available in range set"))
}
})
})
})
+
+// nextip is a convenience function used for testing
+func (i *RangeIter) nextip() net.IP {
+ c, _ := i.Next()
+ if c == nil {
+ return nil
+ }
+
+ return c.IP
+}
types020 "github.com/containernetworking/cni/pkg/types/020"
)
+// The top-level network config, just so we can get the IPAM block
+type Net struct {
+ Name string `json:"name"`
+ CNIVersion string `json:"cniVersion"`
+ IPAM *IPAMConfig `json:"ipam"`
+ Args *struct {
+ A *IPAMArgs `json:"cni"`
+ } `json:"args"`
+}
+
// IPAMConfig represents the IP related network configuration.
// This nests Range because we initially only supported a single
// range directly, and wish to preserve backwards compatability
Routes []*types.Route `json:"routes"`
DataDir string `json:"dataDir"`
ResolvConf string `json:"resolvConf"`
- Ranges []Range `json:"ranges"`
+ Ranges []RangeSet `json:"ranges"`
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
}
IPs []net.IP `json:"ips"`
}
-// The top-level network config, just so we can get the IPAM block
-type Net struct {
- Name string `json:"name"`
- CNIVersion string `json:"cniVersion"`
- IPAM *IPAMConfig `json:"ipam"`
- Args *struct {
- A *IPAMArgs `json:"cni"`
- } `json:"args"`
-}
+type RangeSet []Range
type Range struct {
RangeStart net.IP `json:"rangeStart,omitempty"` // The first ip, inclusive
}
}
- // If a single range (old-style config) is specified, move it to
+ // If a single range (old-style config) is specified, prepend it to
// the Ranges array
if n.IPAM.Range != nil && n.IPAM.Range.Subnet.IP != nil {
- n.IPAM.Ranges = append([]Range{*n.IPAM.Range}, n.IPAM.Ranges...)
+ n.IPAM.Ranges = append([]RangeSet{{*n.IPAM.Range}}, n.IPAM.Ranges...)
}
n.IPAM.Range = nil
numV6 := 0
for i, _ := range n.IPAM.Ranges {
if err := n.IPAM.Ranges[i].Canonicalize(); err != nil {
- return nil, "", fmt.Errorf("Cannot understand range %d: %v", i, err)
+ return nil, "", fmt.Errorf("invalid range set %d: %s", i, err)
}
- if len(n.IPAM.Ranges[i].RangeStart) == 4 {
+
+ if n.IPAM.Ranges[i][0].RangeStart.To4() != nil {
numV4++
} else {
numV6++
if numV4 > 1 || numV6 > 1 {
for _, v := range types020.SupportedVersions {
if n.CNIVersion == v {
- return nil, "", fmt.Errorf("CNI version %v does not support more than 1 range per address family", n.CNIVersion)
+ return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion)
}
}
}
// Check for overlaps
l := len(n.IPAM.Ranges)
- for i, r1 := range n.IPAM.Ranges[:l-1] {
- for j, r2 := range n.IPAM.Ranges[i+1:] {
- if r1.Overlaps(&r2) {
- return nil, "", fmt.Errorf("Range %d overlaps with range %d", i, (i + j + 1))
+ for i, p1 := range n.IPAM.Ranges[:l-1] {
+ for j, p2 := range n.IPAM.Ranges[i+1:] {
+ if p1.Overlaps(&p2) {
+ return nil, "", fmt.Errorf("range set %d overlaps with %d", i, (i + j + 1))
}
}
}
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
- Ranges: []Range{
- {
- RangeStart: net.IP{10, 1, 2, 9},
- RangeEnd: net.IP{10, 1, 2, 20},
- Gateway: net.IP{10, 1, 2, 30},
- Subnet: types.IPNet{
- IP: net.IP{10, 1, 2, 0},
- Mask: net.CIDRMask(24, 32),
+ Ranges: []RangeSet{
+ RangeSet{
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
},
},
},
}))
})
+
It("Should parse a new-style config", func() {
input := `{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24",
- "rangeStart": "10.1.2.9",
- "rangeEnd": "10.1.2.20",
- "gateway": "10.1.2.30"
- },
- {
- "subnet": "11.1.2.0/24",
- "rangeStart": "11.1.2.9",
- "rangeEnd": "11.1.2.20",
- "gateway": "11.1.2.30"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ },
+ {
+ "subnet": "10.1.4.0/24"
+ }
+ ],
+ [{
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }]
+ ]
}
- ]
- }
-}`
+ }`
conf, version, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
Expect(version).Should(Equal("0.3.1"))
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
- Ranges: []Range{
+ Ranges: []RangeSet{
{
- RangeStart: net.IP{10, 1, 2, 9},
- RangeEnd: net.IP{10, 1, 2, 20},
- Gateway: net.IP{10, 1, 2, 30},
- Subnet: types.IPNet{
- IP: net.IP{10, 1, 2, 0},
- Mask: net.CIDRMask(24, 32),
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ {
+ RangeStart: net.IP{10, 1, 4, 1},
+ RangeEnd: net.IP{10, 1, 4, 254},
+ Gateway: net.IP{10, 1, 4, 1},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 4, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
},
},
{
- RangeStart: net.IP{11, 1, 2, 9},
- RangeEnd: net.IP{11, 1, 2, 20},
- Gateway: net.IP{11, 1, 2, 30},
- Subnet: types.IPNet{
- IP: net.IP{11, 1, 2, 0},
- Mask: net.CIDRMask(24, 32),
+ {
+ RangeStart: net.IP{11, 1, 2, 9},
+ RangeEnd: net.IP{11, 1, 2, 20},
+ Gateway: net.IP{11, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{11, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
},
},
},
It("Should parse a mixed config", func() {
input := `{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "subnet": "10.1.2.0/24",
- "rangeStart": "10.1.2.9",
- "rangeEnd": "10.1.2.20",
- "gateway": "10.1.2.30",
- "ranges": [
- {
- "subnet": "11.1.2.0/24",
- "rangeStart": "11.1.2.9",
- "rangeEnd": "11.1.2.20",
- "gateway": "11.1.2.30"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30",
+ "ranges": [[
+ {
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }
+ ]]
}
- ]
- }
-}`
+ }`
conf, version, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
Expect(version).Should(Equal("0.3.1"))
Expect(conf).To(Equal(&IPAMConfig{
Name: "mynet",
Type: "host-local",
- Ranges: []Range{
+ Ranges: []RangeSet{
{
- RangeStart: net.IP{10, 1, 2, 9},
- RangeEnd: net.IP{10, 1, 2, 20},
- Gateway: net.IP{10, 1, 2, 30},
- Subnet: types.IPNet{
- IP: net.IP{10, 1, 2, 0},
- Mask: net.CIDRMask(24, 32),
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
},
},
{
- RangeStart: net.IP{11, 1, 2, 9},
- RangeEnd: net.IP{11, 1, 2, 20},
- Gateway: net.IP{11, 1, 2, 30},
- Subnet: types.IPNet{
- IP: net.IP{11, 1, 2, 0},
- Mask: net.CIDRMask(24, 32),
+ {
+ RangeStart: net.IP{11, 1, 2, 9},
+ RangeEnd: net.IP{11, 1, 2, 20},
+ Gateway: net.IP{11, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{11, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
},
},
},
It("Should parse CNI_ARGS env", func() {
input := `{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24",
- "rangeStart": "10.1.2.9",
- "rangeEnd": "10.1.2.20",
- "gateway": "10.1.2.30"
- },
- {
- "subnet": "11.1.2.0/24",
- "rangeStart": "11.1.2.9",
- "rangeEnd": "11.1.2.20",
- "gateway": "11.1.2.30"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [[
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ }
+ ]]
}
- ]
- }
-}`
+ }`
envArgs := "IP=10.1.2.10"
Expect(conf.IPArgs).To(Equal([]net.IP{{10, 1, 2, 10}}))
})
+
It("Should parse config args", func() {
input := `{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "args": {
- "cni": {
- "ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"]
- }
- },
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24",
- "rangeStart": "10.1.2.9",
- "rangeEnd": "10.1.2.20",
- "gateway": "10.1.2.30"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "args": {
+ "cni": {
+ "ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"]
+ }
},
- {
- "subnet": "11.1.2.0/24",
- "rangeStart": "11.1.2.9",
- "rangeEnd": "11.1.2.20",
- "gateway": "11.1.2.30"
- },
- {
- "subnet": "2001:db8:1::/64"
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [{
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ }],
+ [{
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }],
+ [{
+ "subnet": "2001:db8:1::/64"
+ }]
+ ]
}
- ]
- }
-}`
+ }`
envArgs := "IP=10.1.2.10"
net.ParseIP("2001:db8:1::11"),
}))
})
- It("Should detect overlap", func() {
+
+ It("Should detect overlap between rangesets", func() {
input := `{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24",
- "rangeEnd": "10.1.2.128"
- },
- {
- "subnet": "10.1.2.0/24",
- "rangeStart": "10.1.2.15"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [
+ {"subnet": "10.1.2.0/24"},
+ {"subnet": "10.2.2.0/24"}
+ ],
+ [
+ { "subnet": "10.1.4.0/24"},
+ { "subnet": "10.1.6.0/24"},
+ { "subnet": "10.1.8.0/24"},
+ { "subnet": "10.1.2.0/24"}
+ ]
+ ]
}
- ]
- }
-}`
+ }`
_, _, err := LoadIPAMConfig([]byte(input), "")
- Expect(err).To(MatchError("Range 0 overlaps with range 1"))
+ Expect(err).To(MatchError("range set 0 overlaps with 1"))
})
- It("Should should error on too many ranges", func() {
+ It("Should detect overlap within rangeset", func() {
input := `{
- "cniVersion": "0.2.0",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24"
- },
- {
- "subnet": "11.1.2.0/24"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [
+ { "subnet": "10.1.0.0/22" },
+ { "subnet": "10.1.2.0/24" }
+ ]
+ ]
}
- ]
- }
-}`
+ }`
_, _, err := LoadIPAMConfig([]byte(input), "")
- Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 range per address family"))
+ Expect(err).To(MatchError("invalid range set 0: subnets 10.1.0.1-10.1.3.254 and 10.1.2.1-10.1.2.254 overlap"))
})
- It("Should allow one v4 and v6 range for 0.2.0", func() {
+ It("should error on rangesets with different families", func() {
input := `{
- "cniVersion": "0.2.0",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "ranges": [
- {
- "subnet": "10.1.2.0/24"
- },
- {
- "subnet": "2001:db8:1::/24"
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [
+ { "subnet": "10.1.0.0/22" },
+ { "subnet": "2001:db8:5::/64" }
+ ]
+ ]
}
- ]
- }
-}`
+ }`
+ _, _, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).To(MatchError("invalid range set 0: mixed address families"))
+
+ })
+
+ It("Should should error on too many ranges", func() {
+ input := `{
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [{"subnet": "10.1.2.0/24"}],
+ [{"subnet": "11.1.2.0/24"}]
+ ]
+ }
+ }`
+ _, _, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 address per family"))
+ })
+
+ It("Should allow one v4 and v6 range for 0.2.0", func() {
+ input := `{
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ [{"subnet": "10.1.2.0/24"}],
+ [{"subnet": "2001:db8:1::/24"}]
+ ]
+ }
+ }`
_, _, err := LoadIPAMConfig([]byte(input), "")
Expect(err).NotTo(HaveOccurred())
})
return err
}
- if err := r.IPInRange(r.RangeStart); err != nil {
- return err
+ if !r.Contains(r.RangeStart) {
+ return fmt.Errorf("RangeStart %s not in network %s", r.RangeStart.String(), (*net.IPNet)(&r.Subnet).String())
}
} else {
r.RangeStart = ip.NextIP(r.Subnet.IP)
return err
}
- if err := r.IPInRange(r.RangeEnd); err != nil {
- return err
+ if !r.Contains(r.RangeEnd) {
+ return fmt.Errorf("RangeEnd %s not in network %s", r.RangeEnd.String(), (*net.IPNet)(&r.Subnet).String())
}
} else {
r.RangeEnd = lastIP(r.Subnet)
}
// IsValidIP checks if a given ip is a valid, allocatable address in a given Range
-func (r *Range) IPInRange(addr net.IP) error {
+func (r *Range) Contains(addr net.IP) bool {
if err := canonicalizeIP(&addr); err != nil {
- return err
+ return false
}
subnet := (net.IPNet)(r.Subnet)
+ // Not the same address family
if len(addr) != len(r.Subnet.IP) {
- return fmt.Errorf("IP %s is not the same protocol as subnet %s",
- addr, subnet.String())
+ return false
}
+ // Not in network
if !subnet.Contains(addr) {
- return fmt.Errorf("%s not in network %s", addr, subnet.String())
+ return false
}
// We ignore nils here so we can use this function as we initialize the range.
if r.RangeStart != nil {
+ // Before the range start
if ip.Cmp(addr, r.RangeStart) < 0 {
- return fmt.Errorf("%s is in network %s but before start %s",
- addr, (*net.IPNet)(&r.Subnet).String(), r.RangeStart)
+ return false
}
}
if r.RangeEnd != nil {
if ip.Cmp(addr, r.RangeEnd) > 0 {
- return fmt.Errorf("%s is in network %s but after end %s",
- addr, (*net.IPNet)(&r.Subnet).String(), r.RangeEnd)
+ // After the range end
+ return false
}
}
- return nil
+ return true
}
// Overlaps returns true if there is any overlap between ranges
return false
}
- return r.IPInRange(r1.RangeStart) == nil ||
- r.IPInRange(r1.RangeEnd) == nil ||
- r1.IPInRange(r.RangeStart) == nil ||
- r1.IPInRange(r.RangeEnd) == nil
+ return r.Contains(r1.RangeStart) ||
+ r.Contains(r1.RangeEnd) ||
+ r1.Contains(r.RangeStart) ||
+ r1.Contains(r.RangeEnd)
+}
+
+func (r *Range) String() string {
+ return fmt.Sprintf("%s-%s", r.RangeStart.String(), r.RangeEnd.String())
}
// canonicalizeIP makes sure a provided ip is in standard form
--- /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"
+ "strings"
+)
+
+// Contains returns true if any range in this set contains an IP
+func (s *RangeSet) Contains(addr net.IP) bool {
+ r, _ := s.RangeFor(addr)
+ return r != nil
+}
+
+// RangeFor finds the range that contains an IP, or nil if not found
+func (s *RangeSet) RangeFor(addr net.IP) (*Range, error) {
+ if err := canonicalizeIP(&addr); err != nil {
+ return nil, err
+ }
+
+ for _, r := range *s {
+ if r.Contains(addr) {
+ return &r, nil
+ }
+ }
+
+ return nil, fmt.Errorf("%s not in range set %s", addr.String(), s.String())
+}
+
+// Overlaps returns true if any ranges in any set overlap with this one
+func (s *RangeSet) Overlaps(p1 *RangeSet) bool {
+ for _, r := range *s {
+ for _, r1 := range *p1 {
+ if r.Overlaps(&r1) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// Canonicalize ensures the RangeSet is in a standard form, and detects any
+// invalid input. Call Range.Canonicalize() on every Range in the set
+func (s *RangeSet) Canonicalize() error {
+ if len(*s) == 0 {
+ return fmt.Errorf("empty range set")
+ }
+
+ fam := 0
+ for i, _ := range *s {
+ if err := (*s)[i].Canonicalize(); err != nil {
+ return err
+ }
+ if i == 0 {
+ fam = len((*s)[i].RangeStart)
+ } else {
+ if fam != len((*s)[i].RangeStart) {
+ return fmt.Errorf("mixed address families")
+ }
+ }
+ }
+
+ // Make sure none of the ranges in the set overlap
+ l := len(*s)
+ for i, r1 := range (*s)[:l-1] {
+ for _, r2 := range (*s)[i+1:] {
+ if r1.Overlaps(&r2) {
+ return fmt.Errorf("subnets %s and %s overlap", r1.String(), r2.String())
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *RangeSet) String() string {
+ out := []string{}
+ for _, r := range *s {
+ out = append(out, r.String())
+ }
+
+ return strings.Join(out, ",")
+}
--- /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/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("range sets", func() {
+ It("should detect set membership correctly", func() {
+ p := RangeSet{
+ Range{Subnet: mustSubnet("192.168.0.0/24")},
+ Range{Subnet: mustSubnet("172.16.1.0/24")},
+ }
+
+ err := p.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(p.Contains(net.IP{192, 168, 0, 55})).To(BeTrue())
+
+ r, err := p.RangeFor(net.IP{192, 168, 0, 55})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(r).To(Equal(&p[0]))
+
+ r, err = p.RangeFor(net.IP{192, 168, 99, 99})
+ Expect(r).To(BeNil())
+ Expect(err).To(MatchError("192.168.99.99 not in range set 192.168.0.1-192.168.0.254,172.16.1.1-172.16.1.254"))
+
+ })
+
+ It("should discover overlaps within a set", func() {
+ p := RangeSet{
+ {Subnet: mustSubnet("192.168.0.0/20")},
+ {Subnet: mustSubnet("192.168.2.0/24")},
+ }
+
+ err := p.Canonicalize()
+ Expect(err).To(MatchError("subnets 192.168.0.1-192.168.15.254 and 192.168.2.1-192.168.2.254 overlap"))
+ })
+
+ It("should discover overlaps outside a set", func() {
+ p1 := RangeSet{
+ {Subnet: mustSubnet("192.168.0.0/20")},
+ }
+ p2 := RangeSet{
+ {Subnet: mustSubnet("192.168.2.0/24")},
+ }
+
+ p1.Canonicalize()
+ p2.Canonicalize()
+
+ Expect(p1.Overlaps(&p2)).To(BeTrue())
+ Expect(p2.Overlaps(&p1)).To(BeTrue())
+ })
+})
It("should reject invalid RangeStart and RangeEnd specifications", func() {
r := Range{Subnet: mustSubnet("192.0.2.0/24"), RangeStart: net.ParseIP("192.0.3.0")}
err := r.Canonicalize()
- Expect(err).Should(MatchError("192.0.3.0 not in network 192.0.2.0/24"))
+ Expect(err).Should(MatchError("RangeStart 192.0.3.0 not in network 192.0.2.0/24"))
r = Range{Subnet: mustSubnet("192.0.2.0/24"), RangeEnd: net.ParseIP("192.0.4.0")}
err = r.Canonicalize()
- Expect(err).Should(MatchError("192.0.4.0 not in network 192.0.2.0/24"))
+ Expect(err).Should(MatchError("RangeEnd 192.0.4.0 not in network 192.0.2.0/24"))
r = Range{
Subnet: mustSubnet("192.0.2.0/24"),
RangeEnd: net.ParseIP("192.0.2.40"),
}
err = r.Canonicalize()
- Expect(err).Should(MatchError("192.0.2.50 is in network 192.0.2.0/24 but after end 192.0.2.40"))
+ Expect(err).Should(MatchError("RangeStart 192.0.2.50 not in network 192.0.2.0/24"))
})
It("should reject invalid gateways", func() {
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
- Expect(r.IPInRange(net.ParseIP("192.0.3.0"))).Should(MatchError(
- "192.0.3.0 not in network 192.0.2.0/24"))
+ Expect(r.Contains(net.ParseIP("192.0.3.0"))).Should(BeFalse())
- Expect(r.IPInRange(net.ParseIP("192.0.2.39"))).Should(MatchError(
- "192.0.2.39 is in network 192.0.2.0/24 but before start 192.0.2.40"))
- Expect(r.IPInRange(net.ParseIP("192.0.2.40"))).Should(BeNil())
- Expect(r.IPInRange(net.ParseIP("192.0.2.50"))).Should(BeNil())
- Expect(r.IPInRange(net.ParseIP("192.0.2.51"))).Should(MatchError(
- "192.0.2.51 is in network 192.0.2.0/24 but after end 192.0.2.50"))
+ Expect(r.Contains(net.ParseIP("192.0.2.39"))).Should(BeFalse())
+ Expect(r.Contains(net.ParseIP("192.0.2.40"))).Should(BeTrue())
+ Expect(r.Contains(net.ParseIP("192.0.2.50"))).Should(BeTrue())
+ Expect(r.Contains(net.ParseIP("192.0.2.51"))).Should(BeFalse())
})
It("should accept v6 IPs in range and reject IPs out of range", func() {
}
err := r.Canonicalize()
Expect(err).NotTo(HaveOccurred())
- Expect(r.IPInRange(net.ParseIP("2001:db8:2::"))).Should(MatchError(
- "2001:db8:2:: not in network 2001:db8:1::/64"))
-
- Expect(r.IPInRange(net.ParseIP("2001:db8:1::39"))).Should(MatchError(
- "2001:db8:1::39 is in network 2001:db8:1::/64 but before start 2001:db8:1::40"))
- Expect(r.IPInRange(net.ParseIP("2001:db8:1::40"))).Should(BeNil())
- Expect(r.IPInRange(net.ParseIP("2001:db8:1::50"))).Should(BeNil())
- Expect(r.IPInRange(net.ParseIP("2001:db8:1::51"))).Should(MatchError(
- "2001:db8:1::51 is in network 2001:db8:1::/64 but after end 2001:db8:1::50"))
+ Expect(r.Contains(net.ParseIP("2001:db8:2::"))).Should(BeFalse())
+
+ Expect(r.Contains(net.ParseIP("2001:db8:1::39"))).Should(BeFalse())
+ Expect(r.Contains(net.ParseIP("2001:db8:1::40"))).Should(BeTrue())
+ Expect(r.Contains(net.ParseIP("2001:db8:1::50"))).Should(BeTrue())
+ Expect(r.Contains(net.ParseIP("2001:db8:1::51"))).Should(BeFalse())
})
DescribeTable("Detecting overlap",
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "dataDir": "%s",
+"cniVersion": "0.3.1",
+"name": "mynet",
+"type": "ipvlan",
+"master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
"resolvConf": "%s/resolv.conf",
"ranges": [
- { "subnet": "10.1.2.0/24" },
- { "subnet": "2001:db8:1::0/64" }
+ [{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}],
+ [{ "subnet": "2001:db8:1::0/64" }]
],
"routes": [
{"dst": "0.0.0.0/0"},
{"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
{"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
]
- }
+ }
}`, tmpDir, tmpDir)
args := &skel.CmdArgs{
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
- lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
+ lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
contents, err = ioutil.ReadFile(lastFilePath1)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
- lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.IAENuAABAAAAAAAAAAAAAQ==")
+ lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.1")
contents, err = ioutil.ReadFile(lastFilePath2)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("2001:db8:1::2"))
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.0",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "subnet": "10.1.2.0/24",
- "dataDir": "%s"
- }
+ "cniVersion": "0.3.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
}`, tmpDir)
args := &skel.CmdArgs{
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
- "cniVersion": "0.1.0",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "subnet": "10.1.2.0/24",
- "dataDir": "%s",
- "resolvConf": "%s/resolv.conf"
- }
+ "cniVersion": "0.1.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s",
+ "resolvConf": "%s/resolv.conf"
+ }
}`, tmpDir, tmpDir)
args := &skel.CmdArgs{
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
- lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
+ lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0")
contents, err = ioutil.ReadFile(lastFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "subnet": "10.1.2.0/24",
- "dataDir": "%s"
- }
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
}`, tmpDir)
args := &skel.CmdArgs{
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
- "cniVersion": "0.2.0",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "subnet": "10.1.2.0/24",
- "dataDir": "%s"
- }
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
}`, tmpDir)
args := &skel.CmdArgs{
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "dataDir": "%s",
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
"ranges": [
- { "subnet": "10.1.2.0/24" }
+ [{ "subnet": "10.1.2.0/24" }]
]
- },
+ },
"args": {
"cni": {
"ips": ["10.1.2.88"]
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "dataDir": "%s",
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
"ranges": [
- { "subnet": "10.1.2.0/24" },
- { "subnet": "10.1.3.0/24" }
+ [{ "subnet": "10.1.2.0/24" }],
+ [{ "subnet": "10.1.3.0/24" }]
]
- },
+ },
"args": {
"cni": {
"ips": ["10.1.2.88", "10.1.3.77"]
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "dataDir": "%s",
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
"ranges": [
- { "subnet": "10.1.2.0/24" },
- { "subnet": "2001:db8:1::/24" }
+ [{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }],
+ [{ "subnet": "2001:db8:1::/24" }]
]
- },
+ },
"args": {
"cni": {
"ips": ["10.1.2.88", "2001:db8:1::999"]
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
- "cniVersion": "0.3.1",
- "name": "mynet",
- "type": "ipvlan",
- "master": "foo0",
- "ipam": {
- "type": "host-local",
- "dataDir": "%s",
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
"ranges": [
- { "subnet": "10.1.2.0/24" },
- { "subnet": "10.1.3.0/24" }
+ [{ "subnet": "10.1.2.0/24" }],
+ [{ "subnet": "10.1.3.0/24" }]
]
- },
+ },
"args": {
"cni": {
"ips": ["10.1.2.88", "10.1.2.77"]
requestedIPs[ip.String()] = ip
}
- for idx, ipRange := range ipamConf.Ranges {
- allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
+ for idx, rangeset := range ipamConf.Ranges {
+ allocator := allocator.NewIPAllocator(&rangeset, store, idx)
// Check to see if there are any custom IPs requested in this range.
var requestedIP net.IP
for k, ip := range requestedIPs {
- if ipRange.IPInRange(ip) == nil {
+ if rangeset.Contains(ip) {
requestedIP = ip
delete(requestedIPs, k)
break
// Loop through all ranges, releasing all IPs, even if an error occurs
var errors []string
- for _, ipRange := range ipamConf.Ranges {
- ipAllocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
+ for idx, rangeset := range ipamConf.Ranges {
+ ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)
err := ipAllocator.Release(args.ContainerID)
if err != nil {
rangesStartStr = `,
"ranges": [`
rangeSubnetConfStr = `
- {
+ [{
"subnet": "%s"
- }`
+ }]`
rangeSubnetGWConfStr = `
- {
+ [{
"subnet": "%s",
"gateway": "%s"
- }`
+ }]`
rangesEndStr = `
]`
"ipam": {
"type": "host-local",
"ranges": [
- { "subnet": "10.1.2.0/24"},
- { "subnet": "2001:db8:1::0/66"}
+ [{ "subnet": "10.1.2.0/24"}],
+ [{ "subnet": "2001:db8:1::0/66"}]
]
}
}`