diff --git a/plugins/ipam/host-local/README.md b/plugins/ipam/host-local/README.md index 7ce4a686a..7deb3555c 100644 --- a/plugins/ipam/host-local/README.md +++ b/plugins/ipam/host-local/README.md @@ -8,24 +8,40 @@ it can include a DNS configuration from a `resolv.conf` file on the host. host-local IPAM plugin allocates ip addresses out of a set of address ranges. It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host. +The allocator can allocate multiple ranges, and supports sets of multiple (disjoint) +subnets. The allocation strategy is loosely round-robin within each range set. + ## Example configurations +Note that the key `ranges` is a list of range sets. That is to say, the length +of the top-level array is the number of addresses returned. The second-level +array is a set of subnets to use as a pool of possible addresses. + +This example configuration returns 2 IP addresses. + ```json { "ipam": { "type": "host-local", "ranges": [ - { - "subnet": "10.10.0.0/16", - "rangeStart": "10.10.1.20", - "rangeEnd": "10.10.3.50", - "gateway": "10.10.0.254" - }, - { - "subnet": "3ffe:ffff:0:01ff::/64", - "rangeStart": "3ffe:ffff:0:01ff::0010", - "rangeEnd": "3ffe:ffff:0:01ff::0020" - } + [ + { + "subnet": "10.10.0.0/16", + "rangeStart": "10.10.1.20", + "rangeEnd": "10.10.3.50", + "gateway": "10.10.0.254" + }, + { + "subnet": "172.16.5.0/24" + } + ], + [ + { + "subnet": "3ffe:ffff:0:01ff::/64", + "rangeStart": "3ffe:ffff:0:01ff::0010", + "rangeEnd": "3ffe:ffff:0:01ff::0020" + } + ] ], "routes": [ { "dst": "0.0.0.0/0" }, @@ -58,7 +74,7 @@ deprecated but still supported. We can test it out on the command-line: ```bash -$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ {"subnet": "203.0.113.0/24"}, {"subnet": "2001:db8:1::/64"}], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local +$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ [{"subnet": "203.0.113.0/24"}], [{"subnet": "2001:db8:1::/64"}]], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local ``` @@ -86,7 +102,7 @@ $ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-l * `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used. * `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration * `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers -* `ranges`, (array, required, nonempty) an array of range objects: +* `ranges`, (array, required, nonempty) an array of arrays of range objects: * `subnet` (string, required): CIDR block to allocate out of. * `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block. * `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6 diff --git a/plugins/ipam/host-local/backend/allocator/allocator.go b/plugins/ipam/host-local/backend/allocator/allocator.go index d4c279795..1d2964b9b 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator.go +++ b/plugins/ipam/host-local/backend/allocator/allocator.go @@ -15,11 +15,11 @@ package allocator import ( - "encoding/base64" "fmt" "log" "net" "os" + "strconv" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ip" @@ -27,29 +27,16 @@ import ( ) type IPAllocator struct { - netName string - ipRange Range - store backend.Store - rangeID string // Used for tracking last reserved ip + rangeset *RangeSet + store backend.Store + rangeID string // Used for tracking last reserved ip } -type RangeIter struct { - low net.IP - high net.IP - cur net.IP - start net.IP -} - -func NewIPAllocator(netName string, r Range, store backend.Store) *IPAllocator { - // The range name (last allocated ip suffix) is just the base64 - // encoding of the bytes of the first IP - rangeID := base64.URLEncoding.EncodeToString(r.RangeStart) - +func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator { return &IPAllocator{ - netName: netName, - ipRange: r, - store: store, - rangeID: rangeID, + rangeset: s, + store: store, + rangeID: strconv.Itoa(id), } } @@ -58,27 +45,32 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err a.store.Lock() defer a.store.Unlock() - gw := a.ipRange.Gateway - - var reservedIP net.IP + var reservedIP *net.IPNet + var gw net.IP if requestedIP != nil { - if gw != nil && gw.Equal(requestedIP) { - return nil, fmt.Errorf("requested IP must differ from gateway IP") + if err := canonicalizeIP(&requestedIP); err != nil { + return nil, err } - if err := a.ipRange.IPInRange(requestedIP); err != nil { + r, err := a.rangeset.RangeFor(requestedIP) + if err != nil { return nil, err } + if requestedIP.Equal(r.Gateway) { + return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String()) + } + reserved, err := a.store.Reserve(id, requestedIP, a.rangeID) if err != nil { return nil, err } if !reserved { - return nil, fmt.Errorf("requested IP address %q is not available in network: %s %s", requestedIP, a.netName, (*net.IPNet)(&a.ipRange.Subnet).String()) + return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String()) } - reservedIP = requestedIP + reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask} + gw = r.Gateway } else { iter, err := a.GetIter() @@ -86,39 +78,33 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err return nil, err } for { - cur := iter.Next() - if cur == nil { + reservedIP, gw = iter.Next() + if reservedIP == nil { break } - // don't allocate gateway IP - if gw != nil && cur.Equal(gw) { - continue - } - - reserved, err := a.store.Reserve(id, cur, a.rangeID) + reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID) if err != nil { return nil, err } if reserved { - reservedIP = cur break } } } if reservedIP == nil { - return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String()) + return nil, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String()) } version := "4" - if reservedIP.To4() == nil { + if reservedIP.IP.To4() == nil { version = "6" } return ¤t.IPConfig{ Version: version, - Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask}, + Address: *reservedIP, Gateway: gw, }, nil } @@ -131,15 +117,28 @@ func (a *IPAllocator) Release(id string) error { return a.store.ReleaseByID(id) } +type RangeIter struct { + rangeset *RangeSet + + // The current range id + rangeIdx int + + // Our current position + cur net.IP + + // The IP and range index where we started iterating; if we hit this again, we're done. + startIP net.IP + startRange int +} + // GetIter encapsulates the strategy for this allocator. -// We use a round-robin strategy, attempting to evenly use the whole subnet. +// We use a round-robin strategy, attempting to evenly use the whole set. // More specifically, a crash-looping container will not see the same IP until // the entire range has been run through. // We may wish to consider avoiding recently-released IPs in the future. func (a *IPAllocator) GetIter() (*RangeIter, error) { - i := RangeIter{ - low: a.ipRange.RangeStart, - high: a.ipRange.RangeEnd, + iter := RangeIter{ + rangeset: a.rangeset, } // Round-robin by trying to allocate from the last reserved IP + 1 @@ -151,39 +150,68 @@ func (a *IPAllocator) GetIter() (*RangeIter, error) { if err != nil && !os.IsNotExist(err) { log.Printf("Error retrieving last reserved ip: %v", err) } else if lastReservedIP != nil { - startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil + startFromLastReservedIP = a.rangeset.Contains(lastReservedIP) } + // Find the range in the set with this IP if startFromLastReservedIP { - if i.high.Equal(lastReservedIP) { - i.start = i.low - } else { - i.start = ip.NextIP(lastReservedIP) + for i, r := range *a.rangeset { + if r.Contains(lastReservedIP) { + iter.rangeIdx = i + iter.startRange = i + + // We advance the cursor on every Next(), so the first call + // to next() will return lastReservedIP + 1 + iter.cur = lastReservedIP + break + } } } else { - i.start = a.ipRange.RangeStart + iter.rangeIdx = 0 + iter.startRange = 0 + iter.startIP = (*a.rangeset)[0].RangeStart } - return &i, nil + return &iter, nil } -// Next returns the next IP in the iterator, or nil if end is reached -func (i *RangeIter) Next() net.IP { - // If we're at the beginning, time to start +// Next returns the next IP, its mask, and its gateway. Returns nil +// if the iterator has been exhausted +func (i *RangeIter) Next() (*net.IPNet, net.IP) { + r := (*i.rangeset)[i.rangeIdx] + + // If this is the first time iterating and we're not starting in the middle + // of the range, then start at rangeStart, which is inclusive if i.cur == nil { - i.cur = i.start - return i.cur + i.cur = r.RangeStart + i.startIP = i.cur + if i.cur.Equal(r.Gateway) { + return i.Next() + } + return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway } - // we returned .high last time, since we're inclusive - if i.cur.Equal(i.high) { - i.cur = i.low + + // If we've reached the end of this range, we need to advance the range + // RangeEnd is inclusive as well + if i.cur.Equal(r.RangeEnd) { + i.rangeIdx += 1 + i.rangeIdx %= len(*i.rangeset) + r = (*i.rangeset)[i.rangeIdx] + + i.cur = r.RangeStart } else { i.cur = ip.NextIP(i.cur) } - // If we've looped back to where we started, exit - if i.cur.Equal(i.start) { - return nil + if i.startIP == nil { + i.startIP = i.cur + } else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) { + // IF we've looped back to where we started, give up + return nil, nil + } + + if i.cur.Equal(r.Gateway) { + return i.Next() } - return i.cur + return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway } diff --git a/plugins/ipam/host-local/backend/allocator/allocator_test.go b/plugins/ipam/host-local/backend/allocator/allocator_test.go index 2c258a9ba..436aaa521 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator_test.go +++ b/plugins/ipam/host-local/backend/allocator/allocator_test.go @@ -21,31 +21,29 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" fakestore "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) type AllocatorTestCase struct { - subnet string + subnets []string ipmap map[string]string expectResult string lastIP string } func mkalloc() IPAllocator { - ipnet, _ := types.ParseCIDR("192.168.1.0/24") - - r := Range{ - Subnet: types.IPNet(*ipnet), + p := RangeSet{ + Range{Subnet: mustSubnet("192.168.1.0/29")}, } - r.Canonicalize() + p.Canonicalize() store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{}) alloc := IPAllocator{ - netName: "netname", - ipRange: r, - store: store, - rangeID: "rangeid", + rangeset: &p, + store: store, + rangeID: "rangeid", } return alloc @@ -53,24 +51,23 @@ func mkalloc() IPAllocator { func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) { fmt.Fprintln(GinkgoWriter, "Index:", idx) - subnet, err := types.ParseCIDR(t.subnet) - if err != nil { - return nil, err - } - - conf := Range{ - Subnet: types.IPNet(*subnet), + p := RangeSet{} + for _, s := range t.subnets { + subnet, err := types.ParseCIDR(s) + if err != nil { + return nil, err + } + p = append(p, Range{Subnet: types.IPNet(*subnet)}) } - Expect(conf.Canonicalize()).To(BeNil()) + Expect(p.Canonicalize()).To(BeNil()) store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)}) alloc := IPAllocator{ - "netname", - conf, - store, - "rangeid", + rangeset: &p, + store: store, + rangeID: "rangeid", } return alloc.Get("ID", nil) @@ -79,50 +76,40 @@ func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) { var _ = Describe("host-local ip allocator", func() { Context("RangeIter", func() { It("should loop correctly from the beginning", func() { - r := RangeIter{ - start: net.IP{10, 0, 0, 0}, - low: net.IP{10, 0, 0, 0}, - high: net.IP{10, 0, 0, 5}, - } - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5})) - Expect(r.Next()).To(BeNil()) + a := mkalloc() + r, _ := a.GetIter() + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6})) + Expect(r.nextip()).To(BeNil()) }) It("should loop correctly from the end", func() { - r := RangeIter{ - start: net.IP{10, 0, 0, 5}, - low: net.IP{10, 0, 0, 0}, - high: net.IP{10, 0, 0, 5}, - } - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4})) - Expect(r.Next()).To(BeNil()) + a := mkalloc() + a.store.Reserve("ID", net.IP{192, 168, 1, 6}, a.rangeID) + a.store.ReleaseByID("ID") + r, _ := a.GetIter() + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6})) + Expect(r.nextip()).To(BeNil()) }) - It("should loop correctly from the middle", func() { - r := RangeIter{ - start: net.IP{10, 0, 0, 3}, - low: net.IP{10, 0, 0, 0}, - high: net.IP{10, 0, 0, 5}, - } - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1})) - Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2})) - Expect(r.Next()).To(BeNil()) + a := mkalloc() + a.store.Reserve("ID", net.IP{192, 168, 1, 3}, a.rangeID) + a.store.ReleaseByID("ID") + r, _ := a.GetIter() + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 4})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 5})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 6})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 2})) + Expect(r.nextip()).To(Equal(net.IP{192, 168, 1, 3})) + Expect(r.nextip()).To(BeNil()) }) - }) Context("when has free ip", func() { @@ -130,25 +117,25 @@ var _ = Describe("host-local ip allocator", func() { testCases := []AllocatorTestCase{ // fresh start { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{}, expectResult: "10.0.0.2", lastIP: "", }, { - subnet: "2001:db8:1::0/64", + subnets: []string{"2001:db8:1::0/64"}, ipmap: map[string]string{}, expectResult: "2001:db8:1::2", lastIP: "", }, { - subnet: "10.0.0.0/30", + subnets: []string{"10.0.0.0/30"}, ipmap: map[string]string{}, expectResult: "10.0.0.2", lastIP: "", }, { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.2": "id", }, @@ -157,13 +144,13 @@ var _ = Describe("host-local ip allocator", func() { }, // next ip of last reserved ip { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{}, expectResult: "10.0.0.6", lastIP: "10.0.0.5", }, { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.4": "id", "10.0.0.5": "id", @@ -173,7 +160,7 @@ var _ = Describe("host-local ip allocator", func() { }, // round robin to the beginning { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.6": "id", }, @@ -182,16 +169,17 @@ var _ = Describe("host-local ip allocator", func() { }, // lastIP is out of range { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.2": "id", }, expectResult: "10.0.0.3", lastIP: "10.0.0.128", }, + // subnet is completely full except for lastip // wrap around and reserve lastIP { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.2": "id", "10.0.0.4": "id", @@ -201,6 +189,26 @@ var _ = Describe("host-local ip allocator", func() { expectResult: "10.0.0.3", lastIP: "10.0.0.3", }, + // alocate from multiple subnets + { + subnets: []string{"10.0.0.0/30", "10.0.1.0/30"}, + expectResult: "10.0.0.2", + ipmap: map[string]string{}, + }, + // advance to next subnet + { + subnets: []string{"10.0.0.0/30", "10.0.1.0/30"}, + lastIP: "10.0.0.2", + expectResult: "10.0.1.2", + ipmap: map[string]string{}, + }, + // Roll to start subnet + { + subnets: []string{"10.0.0.0/30", "10.0.1.0/30", "10.0.2.0/30"}, + lastIP: "10.0.2.2", + expectResult: "10.0.0.2", + ipmap: map[string]string{}, + }, } for idx, tc := range testCases { @@ -212,10 +220,10 @@ var _ = Describe("host-local ip allocator", func() { It("should not allocate the broadcast address", func() { alloc := mkalloc() - for i := 2; i < 255; i++ { + for i := 2; i < 7; i++ { res, err := alloc.Get("ID", nil) Expect(err).ToNot(HaveOccurred()) - s := fmt.Sprintf("192.168.1.%d/24", i) + s := fmt.Sprintf("192.168.1.%d/29", i) Expect(s).To(Equal(res.Address.String())) fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String()) } @@ -229,44 +237,17 @@ var _ = Describe("host-local ip allocator", func() { alloc := mkalloc() res, err := alloc.Get("ID", nil) Expect(err).ToNot(HaveOccurred()) - Expect(res.Address.String()).To(Equal("192.168.1.2/24")) + Expect(res.Address.String()).To(Equal("192.168.1.2/29")) err = alloc.Release("ID") Expect(err).ToNot(HaveOccurred()) res, err = alloc.Get("ID", nil) Expect(err).ToNot(HaveOccurred()) - Expect(res.Address.String()).To(Equal("192.168.1.3/24")) + Expect(res.Address.String()).To(Equal("192.168.1.3/29")) }) - It("should allocate RangeStart first", func() { - alloc := mkalloc() - alloc.ipRange.RangeStart = net.IP{192, 168, 1, 10} - res, err := alloc.Get("ID", nil) - Expect(err).ToNot(HaveOccurred()) - Expect(res.Address.String()).To(Equal("192.168.1.10/24")) - - res, err = alloc.Get("ID", nil) - Expect(err).ToNot(HaveOccurred()) - Expect(res.Address.String()).To(Equal("192.168.1.11/24")) - }) - - It("should allocate RangeEnd but not past RangeEnd", func() { - alloc := mkalloc() - alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5} - - for i := 1; i < 5; i++ { - res, err := alloc.Get("ID", nil) - Expect(err).ToNot(HaveOccurred()) - // i+1 because the gateway address is skipped - Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1))) - } - - _, err := alloc.Get("ID", nil) - Expect(err).To(HaveOccurred()) - }) - Context("when requesting a specific IP", func() { It("must allocate the requested IP", func() { alloc := mkalloc() @@ -284,21 +265,21 @@ var _ = Describe("host-local ip allocator", func() { Expect(res.Address.IP.String()).To(Equal(requestedIP.String())) _, err = alloc.Get("ID", requestedIP) - Expect(err).To(MatchError(`requested IP address "192.168.1.5" is not available in network: netname 192.168.1.0/24`)) + Expect(err).To(MatchError(`requested IP address 192.168.1.5 is not available in range set 192.168.1.1-192.168.1.6`)) }) It("must return an error when the requested IP is after RangeEnd", func() { alloc := mkalloc() - alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5} - requestedIP := net.IP{192, 168, 1, 6} + (*alloc.rangeset)[0].RangeEnd = net.IP{192, 168, 1, 4} + requestedIP := net.IP{192, 168, 1, 5} _, err := alloc.Get("ID", requestedIP) Expect(err).To(HaveOccurred()) }) It("must return an error when the requested IP is before RangeStart", func() { alloc := mkalloc() - alloc.ipRange.RangeStart = net.IP{192, 168, 1, 6} - requestedIP := net.IP{192, 168, 1, 5} + (*alloc.rangeset)[0].RangeStart = net.IP{192, 168, 1, 3} + requestedIP := net.IP{192, 168, 1, 2} _, err := alloc.Get("ID", requestedIP) Expect(err).To(HaveOccurred()) }) @@ -309,28 +290,44 @@ var _ = Describe("host-local ip allocator", func() { It("returns a meaningful error", func() { testCases := []AllocatorTestCase{ { - subnet: "10.0.0.0/30", + subnets: []string{"10.0.0.0/30"}, ipmap: map[string]string{ "10.0.0.2": "id", - "10.0.0.3": "id", }, }, { - subnet: "10.0.0.0/29", + subnets: []string{"10.0.0.0/29"}, ipmap: map[string]string{ "10.0.0.2": "id", "10.0.0.3": "id", "10.0.0.4": "id", "10.0.0.5": "id", "10.0.0.6": "id", - "10.0.0.7": "id", + }, + }, + { + subnets: []string{"10.0.0.0/30", "10.0.1.0/30"}, + ipmap: map[string]string{ + "10.0.0.2": "id", + "10.0.1.2": "id", }, }, } for idx, tc := range testCases { _, err := tc.run(idx) - Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet)) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(HavePrefix("no IP addresses available in range set")) } }) }) }) + +// nextip is a convenience function used for testing +func (i *RangeIter) nextip() net.IP { + c, _ := i.Next() + if c == nil { + return nil + } + + return c.IP +} diff --git a/plugins/ipam/host-local/backend/allocator/config.go b/plugins/ipam/host-local/backend/allocator/config.go index 19e2f3ce7..845dad0b3 100644 --- a/plugins/ipam/host-local/backend/allocator/config.go +++ b/plugins/ipam/host-local/backend/allocator/config.go @@ -23,6 +23,16 @@ import ( types020 "github.com/containernetworking/cni/pkg/types/020" ) +// The top-level network config, just so we can get the IPAM block +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + IPAM *IPAMConfig `json:"ipam"` + Args *struct { + A *IPAMArgs `json:"cni"` + } `json:"args"` +} + // IPAMConfig represents the IP related network configuration. // This nests Range because we initially only supported a single // range directly, and wish to preserve backwards compatability @@ -33,7 +43,7 @@ type IPAMConfig struct { Routes []*types.Route `json:"routes"` DataDir string `json:"dataDir"` ResolvConf string `json:"resolvConf"` - Ranges []Range `json:"ranges"` + Ranges []RangeSet `json:"ranges"` IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args } @@ -46,15 +56,7 @@ type IPAMArgs struct { IPs []net.IP `json:"ips"` } -// The top-level network config, just so we can get the IPAM block -type Net struct { - Name string `json:"name"` - CNIVersion string `json:"cniVersion"` - IPAM *IPAMConfig `json:"ipam"` - Args *struct { - A *IPAMArgs `json:"cni"` - } `json:"args"` -} +type RangeSet []Range type Range struct { RangeStart net.IP `json:"rangeStart,omitempty"` // The first ip, inclusive @@ -97,10 +99,10 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { } } - // If a single range (old-style config) is specified, move it to + // If a single range (old-style config) is specified, prepend it to // the Ranges array if n.IPAM.Range != nil && n.IPAM.Range.Subnet.IP != nil { - n.IPAM.Ranges = append([]Range{*n.IPAM.Range}, n.IPAM.Ranges...) + n.IPAM.Ranges = append([]RangeSet{{*n.IPAM.Range}}, n.IPAM.Ranges...) } n.IPAM.Range = nil @@ -113,9 +115,10 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { numV6 := 0 for i, _ := range n.IPAM.Ranges { if err := n.IPAM.Ranges[i].Canonicalize(); err != nil { - return nil, "", fmt.Errorf("Cannot understand range %d: %v", i, err) + return nil, "", fmt.Errorf("invalid range set %d: %s", i, err) } - if len(n.IPAM.Ranges[i].RangeStart) == 4 { + + if n.IPAM.Ranges[i][0].RangeStart.To4() != nil { numV4++ } else { numV6++ @@ -126,17 +129,17 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { if numV4 > 1 || numV6 > 1 { for _, v := range types020.SupportedVersions { if n.CNIVersion == v { - return nil, "", fmt.Errorf("CNI version %v does not support more than 1 range per address family", n.CNIVersion) + return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion) } } } // Check for overlaps l := len(n.IPAM.Ranges) - for i, r1 := range n.IPAM.Ranges[:l-1] { - for j, r2 := range n.IPAM.Ranges[i+1:] { - if r1.Overlaps(&r2) { - return nil, "", fmt.Errorf("Range %d overlaps with range %d", i, (i + j + 1)) + for i, p1 := range n.IPAM.Ranges[:l-1] { + for j, p2 := range n.IPAM.Ranges[i+1:] { + if p1.Overlaps(&p2) { + return nil, "", fmt.Errorf("range set %d overlaps with %d", i, (i + j + 1)) } } } diff --git a/plugins/ipam/host-local/backend/allocator/config_test.go b/plugins/ipam/host-local/backend/allocator/config_test.go index b162ab43e..4631123b2 100644 --- a/plugins/ipam/host-local/backend/allocator/config_test.go +++ b/plugins/ipam/host-local/backend/allocator/config_test.go @@ -44,43 +44,51 @@ var _ = Describe("IPAM config", func() { Expect(conf).To(Equal(&IPAMConfig{ Name: "mynet", Type: "host-local", - Ranges: []Range{ - { - RangeStart: net.IP{10, 1, 2, 9}, - RangeEnd: net.IP{10, 1, 2, 20}, - Gateway: net.IP{10, 1, 2, 30}, - Subnet: types.IPNet{ - IP: net.IP{10, 1, 2, 0}, - Mask: net.CIDRMask(24, 32), + Ranges: []RangeSet{ + RangeSet{ + { + RangeStart: net.IP{10, 1, 2, 9}, + RangeEnd: net.IP{10, 1, 2, 20}, + Gateway: net.IP{10, 1, 2, 30}, + Subnet: types.IPNet{ + IP: net.IP{10, 1, 2, 0}, + Mask: net.CIDRMask(24, 32), + }, }, }, }, })) }) + It("Should parse a new-style config", func() { input := `{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24", - "rangeStart": "10.1.2.9", - "rangeEnd": "10.1.2.20", - "gateway": "10.1.2.30" - }, - { - "subnet": "11.1.2.0/24", - "rangeStart": "11.1.2.9", - "rangeEnd": "11.1.2.20", - "gateway": "11.1.2.30" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [ + { + "subnet": "10.1.2.0/24", + "rangeStart": "10.1.2.9", + "rangeEnd": "10.1.2.20", + "gateway": "10.1.2.30" + }, + { + "subnet": "10.1.4.0/24" + } + ], + [{ + "subnet": "11.1.2.0/24", + "rangeStart": "11.1.2.9", + "rangeEnd": "11.1.2.20", + "gateway": "11.1.2.30" + }] + ] } - ] - } -}` + }` conf, version, err := LoadIPAMConfig([]byte(input), "") Expect(err).NotTo(HaveOccurred()) Expect(version).Should(Equal("0.3.1")) @@ -88,23 +96,36 @@ var _ = Describe("IPAM config", func() { Expect(conf).To(Equal(&IPAMConfig{ Name: "mynet", Type: "host-local", - Ranges: []Range{ + Ranges: []RangeSet{ { - RangeStart: net.IP{10, 1, 2, 9}, - RangeEnd: net.IP{10, 1, 2, 20}, - Gateway: net.IP{10, 1, 2, 30}, - Subnet: types.IPNet{ - IP: net.IP{10, 1, 2, 0}, - Mask: net.CIDRMask(24, 32), + { + RangeStart: net.IP{10, 1, 2, 9}, + RangeEnd: net.IP{10, 1, 2, 20}, + Gateway: net.IP{10, 1, 2, 30}, + Subnet: types.IPNet{ + IP: net.IP{10, 1, 2, 0}, + Mask: net.CIDRMask(24, 32), + }, + }, + { + RangeStart: net.IP{10, 1, 4, 1}, + RangeEnd: net.IP{10, 1, 4, 254}, + Gateway: net.IP{10, 1, 4, 1}, + Subnet: types.IPNet{ + IP: net.IP{10, 1, 4, 0}, + Mask: net.CIDRMask(24, 32), + }, }, }, { - RangeStart: net.IP{11, 1, 2, 9}, - RangeEnd: net.IP{11, 1, 2, 20}, - Gateway: net.IP{11, 1, 2, 30}, - Subnet: types.IPNet{ - IP: net.IP{11, 1, 2, 0}, - Mask: net.CIDRMask(24, 32), + { + RangeStart: net.IP{11, 1, 2, 9}, + RangeEnd: net.IP{11, 1, 2, 20}, + Gateway: net.IP{11, 1, 2, 30}, + Subnet: types.IPNet{ + IP: net.IP{11, 1, 2, 0}, + Mask: net.CIDRMask(24, 32), + }, }, }, }, @@ -113,26 +134,26 @@ var _ = Describe("IPAM config", func() { It("Should parse a mixed config", func() { input := `{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24", - "rangeStart": "10.1.2.9", - "rangeEnd": "10.1.2.20", - "gateway": "10.1.2.30", - "ranges": [ - { - "subnet": "11.1.2.0/24", - "rangeStart": "11.1.2.9", - "rangeEnd": "11.1.2.20", - "gateway": "11.1.2.30" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "rangeStart": "10.1.2.9", + "rangeEnd": "10.1.2.20", + "gateway": "10.1.2.30", + "ranges": [[ + { + "subnet": "11.1.2.0/24", + "rangeStart": "11.1.2.9", + "rangeEnd": "11.1.2.20", + "gateway": "11.1.2.30" + } + ]] } - ] - } -}` + }` conf, version, err := LoadIPAMConfig([]byte(input), "") Expect(err).NotTo(HaveOccurred()) Expect(version).Should(Equal("0.3.1")) @@ -140,23 +161,27 @@ var _ = Describe("IPAM config", func() { Expect(conf).To(Equal(&IPAMConfig{ Name: "mynet", Type: "host-local", - Ranges: []Range{ + Ranges: []RangeSet{ { - RangeStart: net.IP{10, 1, 2, 9}, - RangeEnd: net.IP{10, 1, 2, 20}, - Gateway: net.IP{10, 1, 2, 30}, - Subnet: types.IPNet{ - IP: net.IP{10, 1, 2, 0}, - Mask: net.CIDRMask(24, 32), + { + RangeStart: net.IP{10, 1, 2, 9}, + RangeEnd: net.IP{10, 1, 2, 20}, + Gateway: net.IP{10, 1, 2, 30}, + Subnet: types.IPNet{ + IP: net.IP{10, 1, 2, 0}, + Mask: net.CIDRMask(24, 32), + }, }, }, { - RangeStart: net.IP{11, 1, 2, 9}, - RangeEnd: net.IP{11, 1, 2, 20}, - Gateway: net.IP{11, 1, 2, 30}, - Subnet: types.IPNet{ - IP: net.IP{11, 1, 2, 0}, - Mask: net.CIDRMask(24, 32), + { + RangeStart: net.IP{11, 1, 2, 9}, + RangeEnd: net.IP{11, 1, 2, 20}, + Gateway: net.IP{11, 1, 2, 30}, + Subnet: types.IPNet{ + IP: net.IP{11, 1, 2, 0}, + Mask: net.CIDRMask(24, 32), + }, }, }, }, @@ -165,28 +190,22 @@ var _ = Describe("IPAM config", func() { It("Should parse CNI_ARGS env", func() { input := `{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24", - "rangeStart": "10.1.2.9", - "rangeEnd": "10.1.2.20", - "gateway": "10.1.2.30" - }, - { - "subnet": "11.1.2.0/24", - "rangeStart": "11.1.2.9", - "rangeEnd": "11.1.2.20", - "gateway": "11.1.2.30" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [[ + { + "subnet": "10.1.2.0/24", + "rangeStart": "10.1.2.9", + "rangeEnd": "10.1.2.20", + "gateway": "10.1.2.30" + } + ]] } - ] - } -}` + }` envArgs := "IP=10.1.2.10" @@ -195,38 +214,39 @@ var _ = Describe("IPAM config", func() { Expect(conf.IPArgs).To(Equal([]net.IP{{10, 1, 2, 10}})) }) + It("Should parse config args", func() { input := `{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "args": { - "cni": { - "ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"] - } - }, - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24", - "rangeStart": "10.1.2.9", - "rangeEnd": "10.1.2.20", - "gateway": "10.1.2.30" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "args": { + "cni": { + "ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"] + } }, - { - "subnet": "11.1.2.0/24", - "rangeStart": "11.1.2.9", - "rangeEnd": "11.1.2.20", - "gateway": "11.1.2.30" - }, - { - "subnet": "2001:db8:1::/64" + "ipam": { + "type": "host-local", + "ranges": [ + [{ + "subnet": "10.1.2.0/24", + "rangeStart": "10.1.2.9", + "rangeEnd": "10.1.2.20", + "gateway": "10.1.2.30" + }], + [{ + "subnet": "11.1.2.0/24", + "rangeStart": "11.1.2.9", + "rangeEnd": "11.1.2.20", + "gateway": "11.1.2.30" + }], + [{ + "subnet": "2001:db8:1::/64" + }] + ] } - ] - } -}` + }` envArgs := "IP=10.1.2.10" @@ -239,70 +259,106 @@ var _ = Describe("IPAM config", func() { net.ParseIP("2001:db8:1::11"), })) }) - It("Should detect overlap", func() { + + It("Should detect overlap between rangesets", func() { input := `{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24", - "rangeEnd": "10.1.2.128" - }, - { - "subnet": "10.1.2.0/24", - "rangeStart": "10.1.2.15" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [ + {"subnet": "10.1.2.0/24"}, + {"subnet": "10.2.2.0/24"} + ], + [ + { "subnet": "10.1.4.0/24"}, + { "subnet": "10.1.6.0/24"}, + { "subnet": "10.1.8.0/24"}, + { "subnet": "10.1.2.0/24"} + ] + ] } - ] - } -}` + }` _, _, err := LoadIPAMConfig([]byte(input), "") - Expect(err).To(MatchError("Range 0 overlaps with range 1")) + Expect(err).To(MatchError("range set 0 overlaps with 1")) }) - It("Should should error on too many ranges", func() { + It("Should detect overlap within rangeset", func() { input := `{ - "cniVersion": "0.2.0", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24" - }, - { - "subnet": "11.1.2.0/24" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [ + { "subnet": "10.1.0.0/22" }, + { "subnet": "10.1.2.0/24" } + ] + ] } - ] - } -}` + }` _, _, err := LoadIPAMConfig([]byte(input), "") - Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 range per address family")) + Expect(err).To(MatchError("invalid range set 0: subnets 10.1.0.1-10.1.3.254 and 10.1.2.1-10.1.2.254 overlap")) }) - It("Should allow one v4 and v6 range for 0.2.0", func() { + It("should error on rangesets with different families", func() { input := `{ - "cniVersion": "0.2.0", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "ranges": [ - { - "subnet": "10.1.2.0/24" - }, - { - "subnet": "2001:db8:1::/24" + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [ + { "subnet": "10.1.0.0/22" }, + { "subnet": "2001:db8:5::/64" } + ] + ] } - ] - } -}` + }` + _, _, err := LoadIPAMConfig([]byte(input), "") + Expect(err).To(MatchError("invalid range set 0: mixed address families")) + + }) + + It("Should should error on too many ranges", func() { + input := `{ + "cniVersion": "0.2.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [{"subnet": "10.1.2.0/24"}], + [{"subnet": "11.1.2.0/24"}] + ] + } + }` + _, _, err := LoadIPAMConfig([]byte(input), "") + Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 address per family")) + }) + + It("Should allow one v4 and v6 range for 0.2.0", func() { + input := `{ + "cniVersion": "0.2.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "ranges": [ + [{"subnet": "10.1.2.0/24"}], + [{"subnet": "2001:db8:1::/24"}] + ] + } + }` _, _, err := LoadIPAMConfig([]byte(input), "") Expect(err).NotTo(HaveOccurred()) }) diff --git a/plugins/ipam/host-local/backend/allocator/range.go b/plugins/ipam/host-local/backend/allocator/range.go index 5c7a1ddae..e696b024b 100644 --- a/plugins/ipam/host-local/backend/allocator/range.go +++ b/plugins/ipam/host-local/backend/allocator/range.go @@ -61,8 +61,8 @@ func (r *Range) Canonicalize() error { return err } - if err := r.IPInRange(r.RangeStart); err != nil { - return err + if !r.Contains(r.RangeStart) { + return fmt.Errorf("RangeStart %s not in network %s", r.RangeStart.String(), (*net.IPNet)(&r.Subnet).String()) } } else { r.RangeStart = ip.NextIP(r.Subnet.IP) @@ -75,8 +75,8 @@ func (r *Range) Canonicalize() error { return err } - if err := r.IPInRange(r.RangeEnd); err != nil { - return err + if !r.Contains(r.RangeEnd) { + return fmt.Errorf("RangeEnd %s not in network %s", r.RangeEnd.String(), (*net.IPNet)(&r.Subnet).String()) } } else { r.RangeEnd = lastIP(r.Subnet) @@ -86,38 +86,39 @@ func (r *Range) Canonicalize() error { } // IsValidIP checks if a given ip is a valid, allocatable address in a given Range -func (r *Range) IPInRange(addr net.IP) error { +func (r *Range) Contains(addr net.IP) bool { if err := canonicalizeIP(&addr); err != nil { - return err + return false } subnet := (net.IPNet)(r.Subnet) + // Not the same address family if len(addr) != len(r.Subnet.IP) { - return fmt.Errorf("IP %s is not the same protocol as subnet %s", - addr, subnet.String()) + return false } + // Not in network if !subnet.Contains(addr) { - return fmt.Errorf("%s not in network %s", addr, subnet.String()) + return false } // We ignore nils here so we can use this function as we initialize the range. if r.RangeStart != nil { + // Before the range start if ip.Cmp(addr, r.RangeStart) < 0 { - return fmt.Errorf("%s is in network %s but before start %s", - addr, (*net.IPNet)(&r.Subnet).String(), r.RangeStart) + return false } } if r.RangeEnd != nil { if ip.Cmp(addr, r.RangeEnd) > 0 { - return fmt.Errorf("%s is in network %s but after end %s", - addr, (*net.IPNet)(&r.Subnet).String(), r.RangeEnd) + // After the range end + return false } } - return nil + return true } // Overlaps returns true if there is any overlap between ranges @@ -127,10 +128,14 @@ func (r *Range) Overlaps(r1 *Range) bool { return false } - return r.IPInRange(r1.RangeStart) == nil || - r.IPInRange(r1.RangeEnd) == nil || - r1.IPInRange(r.RangeStart) == nil || - r1.IPInRange(r.RangeEnd) == nil + return r.Contains(r1.RangeStart) || + r.Contains(r1.RangeEnd) || + r1.Contains(r.RangeStart) || + r1.Contains(r.RangeEnd) +} + +func (r *Range) String() string { + return fmt.Sprintf("%s-%s", r.RangeStart.String(), r.RangeEnd.String()) } // canonicalizeIP makes sure a provided ip is in standard form diff --git a/plugins/ipam/host-local/backend/allocator/range_set.go b/plugins/ipam/host-local/backend/allocator/range_set.go new file mode 100644 index 000000000..efe2f9402 --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/range_set.go @@ -0,0 +1,97 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allocator + +import ( + "fmt" + "net" + "strings" +) + +// Contains returns true if any range in this set contains an IP +func (s *RangeSet) Contains(addr net.IP) bool { + r, _ := s.RangeFor(addr) + return r != nil +} + +// RangeFor finds the range that contains an IP, or nil if not found +func (s *RangeSet) RangeFor(addr net.IP) (*Range, error) { + if err := canonicalizeIP(&addr); err != nil { + return nil, err + } + + for _, r := range *s { + if r.Contains(addr) { + return &r, nil + } + } + + return nil, fmt.Errorf("%s not in range set %s", addr.String(), s.String()) +} + +// Overlaps returns true if any ranges in any set overlap with this one +func (s *RangeSet) Overlaps(p1 *RangeSet) bool { + for _, r := range *s { + for _, r1 := range *p1 { + if r.Overlaps(&r1) { + return true + } + } + } + return false +} + +// Canonicalize ensures the RangeSet is in a standard form, and detects any +// invalid input. Call Range.Canonicalize() on every Range in the set +func (s *RangeSet) Canonicalize() error { + if len(*s) == 0 { + return fmt.Errorf("empty range set") + } + + fam := 0 + for i, _ := range *s { + if err := (*s)[i].Canonicalize(); err != nil { + return err + } + if i == 0 { + fam = len((*s)[i].RangeStart) + } else { + if fam != len((*s)[i].RangeStart) { + return fmt.Errorf("mixed address families") + } + } + } + + // Make sure none of the ranges in the set overlap + l := len(*s) + for i, r1 := range (*s)[:l-1] { + for _, r2 := range (*s)[i+1:] { + if r1.Overlaps(&r2) { + return fmt.Errorf("subnets %s and %s overlap", r1.String(), r2.String()) + } + } + } + + return nil +} + +func (s *RangeSet) String() string { + out := []string{} + for _, r := range *s { + out = append(out, r.String()) + } + + return strings.Join(out, ",") +} diff --git a/plugins/ipam/host-local/backend/allocator/range_set_test.go b/plugins/ipam/host-local/backend/allocator/range_set_test.go new file mode 100644 index 000000000..0aced3812 --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/range_set_test.go @@ -0,0 +1,70 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allocator + +import ( + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("range sets", func() { + It("should detect set membership correctly", func() { + p := RangeSet{ + Range{Subnet: mustSubnet("192.168.0.0/24")}, + Range{Subnet: mustSubnet("172.16.1.0/24")}, + } + + err := p.Canonicalize() + Expect(err).NotTo(HaveOccurred()) + + Expect(p.Contains(net.IP{192, 168, 0, 55})).To(BeTrue()) + + r, err := p.RangeFor(net.IP{192, 168, 0, 55}) + Expect(err).NotTo(HaveOccurred()) + Expect(r).To(Equal(&p[0])) + + r, err = p.RangeFor(net.IP{192, 168, 99, 99}) + Expect(r).To(BeNil()) + Expect(err).To(MatchError("192.168.99.99 not in range set 192.168.0.1-192.168.0.254,172.16.1.1-172.16.1.254")) + + }) + + It("should discover overlaps within a set", func() { + p := RangeSet{ + {Subnet: mustSubnet("192.168.0.0/20")}, + {Subnet: mustSubnet("192.168.2.0/24")}, + } + + err := p.Canonicalize() + Expect(err).To(MatchError("subnets 192.168.0.1-192.168.15.254 and 192.168.2.1-192.168.2.254 overlap")) + }) + + It("should discover overlaps outside a set", func() { + p1 := RangeSet{ + {Subnet: mustSubnet("192.168.0.0/20")}, + } + p2 := RangeSet{ + {Subnet: mustSubnet("192.168.2.0/24")}, + } + + p1.Canonicalize() + p2.Canonicalize() + + Expect(p1.Overlaps(&p2)).To(BeTrue()) + Expect(p2.Overlaps(&p1)).To(BeTrue()) + }) +}) diff --git a/plugins/ipam/host-local/backend/allocator/range_test.go b/plugins/ipam/host-local/backend/allocator/range_test.go index 4b61ca5db..cb8ca01be 100644 --- a/plugins/ipam/host-local/backend/allocator/range_test.go +++ b/plugins/ipam/host-local/backend/allocator/range_test.go @@ -77,11 +77,11 @@ var _ = Describe("IP ranges", func() { It("should reject invalid RangeStart and RangeEnd specifications", func() { r := Range{Subnet: mustSubnet("192.0.2.0/24"), RangeStart: net.ParseIP("192.0.3.0")} err := r.Canonicalize() - Expect(err).Should(MatchError("192.0.3.0 not in network 192.0.2.0/24")) + Expect(err).Should(MatchError("RangeStart 192.0.3.0 not in network 192.0.2.0/24")) r = Range{Subnet: mustSubnet("192.0.2.0/24"), RangeEnd: net.ParseIP("192.0.4.0")} err = r.Canonicalize() - Expect(err).Should(MatchError("192.0.4.0 not in network 192.0.2.0/24")) + Expect(err).Should(MatchError("RangeEnd 192.0.4.0 not in network 192.0.2.0/24")) r = Range{ Subnet: mustSubnet("192.0.2.0/24"), @@ -89,7 +89,7 @@ var _ = Describe("IP ranges", func() { RangeEnd: net.ParseIP("192.0.2.40"), } err = r.Canonicalize() - Expect(err).Should(MatchError("192.0.2.50 is in network 192.0.2.0/24 but after end 192.0.2.40")) + Expect(err).Should(MatchError("RangeStart 192.0.2.50 not in network 192.0.2.0/24")) }) It("should reject invalid gateways", func() { @@ -126,15 +126,12 @@ var _ = Describe("IP ranges", func() { err := r.Canonicalize() Expect(err).NotTo(HaveOccurred()) - Expect(r.IPInRange(net.ParseIP("192.0.3.0"))).Should(MatchError( - "192.0.3.0 not in network 192.0.2.0/24")) + Expect(r.Contains(net.ParseIP("192.0.3.0"))).Should(BeFalse()) - Expect(r.IPInRange(net.ParseIP("192.0.2.39"))).Should(MatchError( - "192.0.2.39 is in network 192.0.2.0/24 but before start 192.0.2.40")) - Expect(r.IPInRange(net.ParseIP("192.0.2.40"))).Should(BeNil()) - Expect(r.IPInRange(net.ParseIP("192.0.2.50"))).Should(BeNil()) - Expect(r.IPInRange(net.ParseIP("192.0.2.51"))).Should(MatchError( - "192.0.2.51 is in network 192.0.2.0/24 but after end 192.0.2.50")) + Expect(r.Contains(net.ParseIP("192.0.2.39"))).Should(BeFalse()) + Expect(r.Contains(net.ParseIP("192.0.2.40"))).Should(BeTrue()) + Expect(r.Contains(net.ParseIP("192.0.2.50"))).Should(BeTrue()) + Expect(r.Contains(net.ParseIP("192.0.2.51"))).Should(BeFalse()) }) It("should accept v6 IPs in range and reject IPs out of range", func() { @@ -145,15 +142,12 @@ var _ = Describe("IP ranges", func() { } err := r.Canonicalize() Expect(err).NotTo(HaveOccurred()) - Expect(r.IPInRange(net.ParseIP("2001:db8:2::"))).Should(MatchError( - "2001:db8:2:: not in network 2001:db8:1::/64")) - - Expect(r.IPInRange(net.ParseIP("2001:db8:1::39"))).Should(MatchError( - "2001:db8:1::39 is in network 2001:db8:1::/64 but before start 2001:db8:1::40")) - Expect(r.IPInRange(net.ParseIP("2001:db8:1::40"))).Should(BeNil()) - Expect(r.IPInRange(net.ParseIP("2001:db8:1::50"))).Should(BeNil()) - Expect(r.IPInRange(net.ParseIP("2001:db8:1::51"))).Should(MatchError( - "2001:db8:1::51 is in network 2001:db8:1::/64 but after end 2001:db8:1::50")) + Expect(r.Contains(net.ParseIP("2001:db8:2::"))).Should(BeFalse()) + + Expect(r.Contains(net.ParseIP("2001:db8:1::39"))).Should(BeFalse()) + Expect(r.Contains(net.ParseIP("2001:db8:1::40"))).Should(BeTrue()) + Expect(r.Contains(net.ParseIP("2001:db8:1::50"))).Should(BeTrue()) + Expect(r.Contains(net.ParseIP("2001:db8:1::51"))).Should(BeFalse()) }) DescribeTable("Detecting overlap", diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go index 30b73c819..3e4b0d866 100644 --- a/plugins/ipam/host-local/host_local_test.go +++ b/plugins/ipam/host-local/host_local_test.go @@ -45,17 +45,17 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "dataDir": "%s", +"cniVersion": "0.3.1", +"name": "mynet", +"type": "ipvlan", +"master": "foo0", + "ipam": { + "type": "host-local", + "dataDir": "%s", "resolvConf": "%s/resolv.conf", "ranges": [ - { "subnet": "10.1.2.0/24" }, - { "subnet": "2001:db8:1::0/64" } + [{ "subnet": "10.1.2.0/24" }, {"subnet": "10.2.2.0/24"}], + [{ "subnet": "2001:db8:1::0/64" }] ], "routes": [ {"dst": "0.0.0.0/0"}, @@ -63,7 +63,7 @@ var _ = Describe("host-local Operations", func() { {"dst": "192.168.0.0/16", "gw": "1.1.1.1"}, {"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"} ] - } + } }`, tmpDir, tmpDir) args := &skel.CmdArgs{ @@ -117,12 +117,12 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("dummy")) - lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==") + lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0") contents, err = ioutil.ReadFile(lastFilePath1) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("10.1.2.2")) - lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.IAENuAABAAAAAAAAAAAAAQ==") + lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.1") contents, err = ioutil.ReadFile(lastFilePath2) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("2001:db8:1::2")) @@ -147,15 +147,15 @@ var _ = Describe("host-local Operations", func() { defer os.RemoveAll(tmpDir) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.0", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24", - "dataDir": "%s" - } + "cniVersion": "0.3.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s" + } }`, tmpDir) args := &skel.CmdArgs{ @@ -184,16 +184,16 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.1.0", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24", - "dataDir": "%s", - "resolvConf": "%s/resolv.conf" - } + "cniVersion": "0.1.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s", + "resolvConf": "%s/resolv.conf" + } }`, tmpDir, tmpDir) args := &skel.CmdArgs{ @@ -224,7 +224,7 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("dummy")) - lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==") + lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.0") contents, err = ioutil.ReadFile(lastFilePath) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("10.1.2.2")) @@ -250,15 +250,15 @@ var _ = Describe("host-local Operations", func() { defer os.RemoveAll(tmpDir) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24", - "dataDir": "%s" - } + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s" + } }`, tmpDir) args := &skel.CmdArgs{ @@ -301,15 +301,15 @@ var _ = Describe("host-local Operations", func() { defer os.RemoveAll(tmpDir) conf := fmt.Sprintf(`{ - "cniVersion": "0.2.0", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24", - "dataDir": "%s" - } + "cniVersion": "0.2.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s" + } }`, tmpDir) args := &skel.CmdArgs{ @@ -336,17 +336,17 @@ var _ = Describe("host-local Operations", func() { defer os.RemoveAll(tmpDir) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "dataDir": "%s", + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "dataDir": "%s", "ranges": [ - { "subnet": "10.1.2.0/24" } + [{ "subnet": "10.1.2.0/24" }] ] - }, + }, "args": { "cni": { "ips": ["10.1.2.88"] @@ -384,18 +384,18 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "dataDir": "%s", + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "dataDir": "%s", "ranges": [ - { "subnet": "10.1.2.0/24" }, - { "subnet": "10.1.3.0/24" } + [{ "subnet": "10.1.2.0/24" }], + [{ "subnet": "10.1.3.0/24" }] ] - }, + }, "args": { "cni": { "ips": ["10.1.2.88", "10.1.3.77"] @@ -434,18 +434,18 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "dataDir": "%s", + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "dataDir": "%s", "ranges": [ - { "subnet": "10.1.2.0/24" }, - { "subnet": "2001:db8:1::/24" } + [{"subnet":"172.16.1.0/24"}, { "subnet": "10.1.2.0/24" }], + [{ "subnet": "2001:db8:1::/24" }] ] - }, + }, "args": { "cni": { "ips": ["10.1.2.88", "2001:db8:1::999"] @@ -481,18 +481,18 @@ var _ = Describe("host-local Operations", func() { defer os.RemoveAll(tmpDir) conf := fmt.Sprintf(`{ - "cniVersion": "0.3.1", - "name": "mynet", - "type": "ipvlan", - "master": "foo0", - "ipam": { - "type": "host-local", - "dataDir": "%s", + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "dataDir": "%s", "ranges": [ - { "subnet": "10.1.2.0/24" }, - { "subnet": "10.1.3.0/24" } + [{ "subnet": "10.1.2.0/24" }], + [{ "subnet": "10.1.3.0/24" }] ] - }, + }, "args": { "cni": { "ips": ["10.1.2.88", "10.1.2.77"] diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index e554a7077..9e2bacc23 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -66,13 +66,13 @@ func cmdAdd(args *skel.CmdArgs) error { requestedIPs[ip.String()] = ip } - for idx, ipRange := range ipamConf.Ranges { - allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store) + for idx, rangeset := range ipamConf.Ranges { + allocator := allocator.NewIPAllocator(&rangeset, store, idx) // Check to see if there are any custom IPs requested in this range. var requestedIP net.IP for k, ip := range requestedIPs { - if ipRange.IPInRange(ip) == nil { + if rangeset.Contains(ip) { requestedIP = ip delete(requestedIPs, k) break @@ -124,8 +124,8 @@ func cmdDel(args *skel.CmdArgs) error { // Loop through all ranges, releasing all IPs, even if an error occurs var errors []string - for _, ipRange := range ipamConf.Ranges { - ipAllocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store) + for idx, rangeset := range ipamConf.Ranges { + ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx) err := ipAllocator.Release(args.ContainerID) if err != nil { diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 2939b12c1..13e7257ab 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -94,14 +94,14 @@ const ( rangesStartStr = `, "ranges": [` rangeSubnetConfStr = ` - { + [{ "subnet": "%s" - }` + }]` rangeSubnetGWConfStr = ` - { + [{ "subnet": "%s", "gateway": "%s" - }` + }]` rangesEndStr = ` ]` diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index ac77d6301..e68c630ce 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -155,8 +155,8 @@ var _ = Describe("ptp Operations", func() { "ipam": { "type": "host-local", "ranges": [ - { "subnet": "10.1.2.0/24"}, - { "subnet": "2001:db8:1::0/66"} + [{ "subnet": "10.1.2.0/24"}], + [{ "subnet": "2001:db8:1::0/66"}] ] } }`