Skip to content

Commit 7480240

Browse files
authored
Merge pull request #50 from squeed/host-local-multi
ipam/host-local: support sets of disjoint ranges
2 parents 5544d9c + 27d027a commit 7480240

13 files changed

Lines changed: 785 additions & 519 deletions

File tree

plugins/ipam/host-local/README.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,40 @@ it can include a DNS configuration from a `resolv.conf` file on the host.
88
host-local IPAM plugin allocates ip addresses out of a set of address ranges.
99
It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
1010

11+
The allocator can allocate multiple ranges, and supports sets of multiple (disjoint)
12+
subnets. The allocation strategy is loosely round-robin within each range set.
13+
1114
## Example configurations
1215

16+
Note that the key `ranges` is a list of range sets. That is to say, the length
17+
of the top-level array is the number of addresses returned. The second-level
18+
array is a set of subnets to use as a pool of possible addresses.
19+
20+
This example configuration returns 2 IP addresses.
21+
1322
```json
1423
{
1524
"ipam": {
1625
"type": "host-local",
1726
"ranges": [
18-
{
19-
"subnet": "10.10.0.0/16",
20-
"rangeStart": "10.10.1.20",
21-
"rangeEnd": "10.10.3.50",
22-
"gateway": "10.10.0.254"
23-
},
24-
{
25-
"subnet": "3ffe:ffff:0:01ff::/64",
26-
"rangeStart": "3ffe:ffff:0:01ff::0010",
27-
"rangeEnd": "3ffe:ffff:0:01ff::0020"
28-
}
27+
[
28+
{
29+
"subnet": "10.10.0.0/16",
30+
"rangeStart": "10.10.1.20",
31+
"rangeEnd": "10.10.3.50",
32+
"gateway": "10.10.0.254"
33+
},
34+
{
35+
"subnet": "172.16.5.0/24"
36+
}
37+
],
38+
[
39+
{
40+
"subnet": "3ffe:ffff:0:01ff::/64",
41+
"rangeStart": "3ffe:ffff:0:01ff::0010",
42+
"rangeEnd": "3ffe:ffff:0:01ff::0020"
43+
}
44+
]
2945
],
3046
"routes": [
3147
{ "dst": "0.0.0.0/0" },
@@ -58,7 +74,7 @@ deprecated but still supported.
5874
We can test it out on the command-line:
5975

6076
```bash
61-
$ 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
77+
$ 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
6278

6379
```
6480

@@ -86,7 +102,7 @@ $ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-l
86102
* `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.
87103
* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
88104
* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
89-
* `ranges`, (array, required, nonempty) an array of range objects:
105+
* `ranges`, (array, required, nonempty) an array of arrays of range objects:
90106
* `subnet` (string, required): CIDR block to allocate out of.
91107
* `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
92108
* `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

plugins/ipam/host-local/backend/allocator/allocator.go

Lines changed: 92 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,28 @@
1515
package allocator
1616

1717
import (
18-
"encoding/base64"
1918
"fmt"
2019
"log"
2120
"net"
2221
"os"
22+
"strconv"
2323

2424
"github.com/containernetworking/cni/pkg/types/current"
2525
"github.com/containernetworking/plugins/pkg/ip"
2626
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
2727
)
2828

2929
type IPAllocator struct {
30-
netName string
31-
ipRange Range
32-
store backend.Store
33-
rangeID string // Used for tracking last reserved ip
30+
rangeset *RangeSet
31+
store backend.Store
32+
rangeID string // Used for tracking last reserved ip
3433
}
3534

36-
type RangeIter struct {
37-
low net.IP
38-
high net.IP
39-
cur net.IP
40-
start net.IP
41-
}
42-
43-
func NewIPAllocator(netName string, r Range, store backend.Store) *IPAllocator {
44-
// The range name (last allocated ip suffix) is just the base64
45-
// encoding of the bytes of the first IP
46-
rangeID := base64.URLEncoding.EncodeToString(r.RangeStart)
47-
35+
func NewIPAllocator(s *RangeSet, store backend.Store, id int) *IPAllocator {
4836
return &IPAllocator{
49-
netName: netName,
50-
ipRange: r,
51-
store: store,
52-
rangeID: rangeID,
37+
rangeset: s,
38+
store: store,
39+
rangeID: strconv.Itoa(id),
5340
}
5441
}
5542

@@ -58,67 +45,66 @@ func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, err
5845
a.store.Lock()
5946
defer a.store.Unlock()
6047

61-
gw := a.ipRange.Gateway
62-
63-
var reservedIP net.IP
48+
var reservedIP *net.IPNet
49+
var gw net.IP
6450

6551
if requestedIP != nil {
66-
if gw != nil && gw.Equal(requestedIP) {
67-
return nil, fmt.Errorf("requested IP must differ from gateway IP")
52+
if err := canonicalizeIP(&requestedIP); err != nil {
53+
return nil, err
6854
}
6955

70-
if err := a.ipRange.IPInRange(requestedIP); err != nil {
56+
r, err := a.rangeset.RangeFor(requestedIP)
57+
if err != nil {
7158
return nil, err
7259
}
7360

61+
if requestedIP.Equal(r.Gateway) {
62+
return nil, fmt.Errorf("requested ip %s is subnet's gateway", requestedIP.String())
63+
}
64+
7465
reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
7566
if err != nil {
7667
return nil, err
7768
}
7869
if !reserved {
79-
return nil, fmt.Errorf("requested IP address %q is not available in network: %s %s", requestedIP, a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
70+
return nil, fmt.Errorf("requested IP address %s is not available in range set %s", requestedIP, a.rangeset.String())
8071
}
81-
reservedIP = requestedIP
72+
reservedIP = &net.IPNet{IP: requestedIP, Mask: r.Subnet.Mask}
73+
gw = r.Gateway
8274

8375
} else {
8476
iter, err := a.GetIter()
8577
if err != nil {
8678
return nil, err
8779
}
8880
for {
89-
cur := iter.Next()
90-
if cur == nil {
81+
reservedIP, gw = iter.Next()
82+
if reservedIP == nil {
9183
break
9284
}
9385

94-
// don't allocate gateway IP
95-
if gw != nil && cur.Equal(gw) {
96-
continue
97-
}
98-
99-
reserved, err := a.store.Reserve(id, cur, a.rangeID)
86+
reserved, err := a.store.Reserve(id, reservedIP.IP, a.rangeID)
10087
if err != nil {
10188
return nil, err
10289
}
10390

10491
if reserved {
105-
reservedIP = cur
10692
break
10793
}
10894
}
10995
}
11096

11197
if reservedIP == nil {
112-
return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
98+
return nil, fmt.Errorf("no IP addresses available in range set: %s", a.rangeset.String())
11399
}
114100
version := "4"
115-
if reservedIP.To4() == nil {
101+
if reservedIP.IP.To4() == nil {
116102
version = "6"
117103
}
118104

119105
return &current.IPConfig{
120106
Version: version,
121-
Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask},
107+
Address: *reservedIP,
122108
Gateway: gw,
123109
}, nil
124110
}
@@ -131,15 +117,28 @@ func (a *IPAllocator) Release(id string) error {
131117
return a.store.ReleaseByID(id)
132118
}
133119

120+
type RangeIter struct {
121+
rangeset *RangeSet
122+
123+
// The current range id
124+
rangeIdx int
125+
126+
// Our current position
127+
cur net.IP
128+
129+
// The IP and range index where we started iterating; if we hit this again, we're done.
130+
startIP net.IP
131+
startRange int
132+
}
133+
134134
// GetIter encapsulates the strategy for this allocator.
135-
// We use a round-robin strategy, attempting to evenly use the whole subnet.
135+
// We use a round-robin strategy, attempting to evenly use the whole set.
136136
// More specifically, a crash-looping container will not see the same IP until
137137
// the entire range has been run through.
138138
// We may wish to consider avoiding recently-released IPs in the future.
139139
func (a *IPAllocator) GetIter() (*RangeIter, error) {
140-
i := RangeIter{
141-
low: a.ipRange.RangeStart,
142-
high: a.ipRange.RangeEnd,
140+
iter := RangeIter{
141+
rangeset: a.rangeset,
143142
}
144143

145144
// Round-robin by trying to allocate from the last reserved IP + 1
@@ -151,39 +150,68 @@ func (a *IPAllocator) GetIter() (*RangeIter, error) {
151150
if err != nil && !os.IsNotExist(err) {
152151
log.Printf("Error retrieving last reserved ip: %v", err)
153152
} else if lastReservedIP != nil {
154-
startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
153+
startFromLastReservedIP = a.rangeset.Contains(lastReservedIP)
155154
}
156155

156+
// Find the range in the set with this IP
157157
if startFromLastReservedIP {
158-
if i.high.Equal(lastReservedIP) {
159-
i.start = i.low
160-
} else {
161-
i.start = ip.NextIP(lastReservedIP)
158+
for i, r := range *a.rangeset {
159+
if r.Contains(lastReservedIP) {
160+
iter.rangeIdx = i
161+
iter.startRange = i
162+
163+
// We advance the cursor on every Next(), so the first call
164+
// to next() will return lastReservedIP + 1
165+
iter.cur = lastReservedIP
166+
break
167+
}
162168
}
163169
} else {
164-
i.start = a.ipRange.RangeStart
170+
iter.rangeIdx = 0
171+
iter.startRange = 0
172+
iter.startIP = (*a.rangeset)[0].RangeStart
165173
}
166-
return &i, nil
174+
return &iter, nil
167175
}
168176

169-
// Next returns the next IP in the iterator, or nil if end is reached
170-
func (i *RangeIter) Next() net.IP {
171-
// If we're at the beginning, time to start
177+
// Next returns the next IP, its mask, and its gateway. Returns nil
178+
// if the iterator has been exhausted
179+
func (i *RangeIter) Next() (*net.IPNet, net.IP) {
180+
r := (*i.rangeset)[i.rangeIdx]
181+
182+
// If this is the first time iterating and we're not starting in the middle
183+
// of the range, then start at rangeStart, which is inclusive
172184
if i.cur == nil {
173-
i.cur = i.start
174-
return i.cur
185+
i.cur = r.RangeStart
186+
i.startIP = i.cur
187+
if i.cur.Equal(r.Gateway) {
188+
return i.Next()
189+
}
190+
return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
175191
}
176-
// we returned .high last time, since we're inclusive
177-
if i.cur.Equal(i.high) {
178-
i.cur = i.low
192+
193+
// If we've reached the end of this range, we need to advance the range
194+
// RangeEnd is inclusive as well
195+
if i.cur.Equal(r.RangeEnd) {
196+
i.rangeIdx += 1
197+
i.rangeIdx %= len(*i.rangeset)
198+
r = (*i.rangeset)[i.rangeIdx]
199+
200+
i.cur = r.RangeStart
179201
} else {
180202
i.cur = ip.NextIP(i.cur)
181203
}
182204

183-
// If we've looped back to where we started, exit
184-
if i.cur.Equal(i.start) {
185-
return nil
205+
if i.startIP == nil {
206+
i.startIP = i.cur
207+
} else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) {
208+
// IF we've looped back to where we started, give up
209+
return nil, nil
210+
}
211+
212+
if i.cur.Equal(r.Gateway) {
213+
return i.Next()
186214
}
187215

188-
return i.cur
216+
return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
189217
}

0 commit comments

Comments
 (0)