plugins/ipam: round robin ip allocation for host-local ipam
This changes the ip allocation logic to round robin. Before this, host-local IPAM searched for available IPs from start of subnet. Hence it tends to allocate IPs that had been used recently. This is not ideal since it may cause collisions.
This commit is contained in:

committed by
Stefan Junker

parent
5c3c171642
commit
2445a960a9
@ -16,6 +16,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ip"
|
||||
@ -57,7 +58,6 @@ func NewIPAllocator(conf *IPAMConfig, store backend.Store) (*IPAllocator, error)
|
||||
// RangeEnd is inclusive
|
||||
end = ip.NextIP(conf.RangeEnd)
|
||||
}
|
||||
|
||||
return &IPAllocator{start, end, conf, store}, nil
|
||||
}
|
||||
|
||||
@ -112,7 +112,8 @@ func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
|
||||
return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
|
||||
}
|
||||
|
||||
for cur := a.start; !cur.Equal(a.end); cur = ip.NextIP(cur) {
|
||||
startIP, endIP := a.getSearchRange()
|
||||
for cur := startIP; !cur.Equal(endIP); cur = a.nextIP(cur) {
|
||||
// don't allocate gateway IP
|
||||
if gw != nil && cur.Equal(gw) {
|
||||
continue
|
||||
@ -163,3 +164,39 @@ func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) {
|
||||
}
|
||||
return ipnet.IP, end, nil
|
||||
}
|
||||
|
||||
// nextIP returns the next ip of curIP within ipallocator's subnet
|
||||
func (a *IPAllocator) nextIP(curIP net.IP) net.IP {
|
||||
if curIP.Equal(a.end) {
|
||||
return a.start
|
||||
}
|
||||
return ip.NextIP(curIP)
|
||||
}
|
||||
|
||||
// getSearchRange returns the start and end ip based on the last reserved ip
|
||||
func (a *IPAllocator) getSearchRange() (net.IP, net.IP) {
|
||||
var startIP net.IP
|
||||
var endIP net.IP
|
||||
startFromLastReservedIP := false
|
||||
lastReservedIP, err := a.store.LastReservedIP()
|
||||
if err != nil {
|
||||
log.Printf("Error retriving last reserved ip: %v", err)
|
||||
} else if lastReservedIP != nil {
|
||||
subnet := net.IPNet{
|
||||
IP: a.conf.Subnet.IP,
|
||||
Mask: a.conf.Subnet.Mask,
|
||||
}
|
||||
err := validateRangeIP(lastReservedIP, &subnet)
|
||||
if err == nil {
|
||||
startFromLastReservedIP = true
|
||||
}
|
||||
}
|
||||
if startFromLastReservedIP {
|
||||
startIP = a.nextIP(lastReservedIP)
|
||||
endIP = lastReservedIP
|
||||
} else {
|
||||
startIP = a.start
|
||||
endIP = a.end
|
||||
}
|
||||
return startIP, endIP
|
||||
}
|
||||
|
136
plugins/ipam/host-local/allocator_test.go
Normal file
136
plugins/ipam/host-local/allocator_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
fakestore "github.com/containernetworking/cni/plugins/ipam/host-local/backend/testing"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net"
|
||||
)
|
||||
|
||||
type AllocatorTestCase struct {
|
||||
subnet string
|
||||
ipmap map[string]string
|
||||
expectResult string
|
||||
lastIP string
|
||||
}
|
||||
|
||||
func (t AllocatorTestCase) run() (*types.IPConfig, error) {
|
||||
subnet, err := types.ParseCIDR(t.subnet)
|
||||
conf := IPAMConfig{
|
||||
Name: "test",
|
||||
Type: "host-local",
|
||||
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
|
||||
}
|
||||
store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP))
|
||||
alloc, _ := NewIPAllocator(&conf, store)
|
||||
res, err := alloc.Get("ID")
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ = Describe("host-local ip allocator", func() {
|
||||
Context("when has free ip", func() {
|
||||
It("should allocate ips in round robin", func() {
|
||||
testCases := []AllocatorTestCase{
|
||||
// fresh start
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "10.0.0.2",
|
||||
lastIP: "",
|
||||
},
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
},
|
||||
expectResult: "10.0.0.3",
|
||||
lastIP: "",
|
||||
},
|
||||
// next ip of last reserved ip
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{},
|
||||
expectResult: "10.0.0.7",
|
||||
lastIP: "10.0.0.6",
|
||||
},
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.5": "id",
|
||||
"10.0.0.6": "id",
|
||||
},
|
||||
expectResult: "10.0.0.7",
|
||||
lastIP: "10.0.0.4",
|
||||
},
|
||||
// round robin to the beginning
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.7": "id",
|
||||
},
|
||||
expectResult: "10.0.0.2",
|
||||
lastIP: "10.0.0.6",
|
||||
},
|
||||
// lastIP is out of range
|
||||
{
|
||||
subnet: "10.0.0.0/29",
|
||||
ipmap: map[string]string{
|
||||
"10.0.0.2": "id",
|
||||
},
|
||||
expectResult: "10.0.0.3",
|
||||
lastIP: "10.0.0.128",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
res, err := tc.run()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(res.IP.IP.String()).To(Equal(tc.expectResult))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Context("when out of ips", func() {
|
||||
It("returns a meaningful error", func() {
|
||||
testCases := []AllocatorTestCase{
|
||||
{
|
||||
subnet: "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",
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
_, err := tc.run()
|
||||
Expect(err).To(MatchError("no IP addresses available in network: test"))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
@ -15,12 +15,15 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const lastIPFile = "last_reserved_ip"
|
||||
|
||||
var defaultDataDir = "/var/lib/cni/networks"
|
||||
|
||||
type Store struct {
|
||||
@ -59,9 +62,25 @@ func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
|
||||
os.Remove(f.Name())
|
||||
return false, err
|
||||
}
|
||||
// store the reserved ip in lastIPFile
|
||||
ipfile := filepath.Join(s.dataDir, lastIPFile)
|
||||
err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// LastReservedIP returns the last reserved IP if exists
|
||||
func (s *Store) LastReservedIP() (net.IP, error) {
|
||||
ipfile := filepath.Join(s.dataDir, lastIPFile)
|
||||
data, err := ioutil.ReadFile(ipfile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to retrieve last reserved ip: %v", err)
|
||||
}
|
||||
return net.ParseIP(string(data)), nil
|
||||
}
|
||||
|
||||
func (s *Store) Release(ip net.IP) error {
|
||||
return os.Remove(filepath.Join(s.dataDir, ip.String()))
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type Store interface {
|
||||
Unlock() error
|
||||
Close() error
|
||||
Reserve(id string, ip net.IP) (bool, error)
|
||||
LastReservedIP() (net.IP, error)
|
||||
Release(ip net.IP) error
|
||||
ReleaseByID(id string) error
|
||||
}
|
||||
|
72
plugins/ipam/host-local/backend/testing/fake_store.go
Normal file
72
plugins/ipam/host-local/backend/testing/fake_store.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2015 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 testing
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type FakeStore struct {
|
||||
ipMap map[string]string
|
||||
lastReservedIP net.IP
|
||||
}
|
||||
|
||||
func NewFakeStore(ipmap map[string]string, lastIP net.IP) *FakeStore {
|
||||
return &FakeStore{ipmap, lastIP}
|
||||
}
|
||||
|
||||
func (s *FakeStore) Lock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Reserve(id string, ip net.IP) (bool, error) {
|
||||
key := ip.String()
|
||||
if _, ok := s.ipMap[key]; !ok {
|
||||
s.ipMap[key] = id
|
||||
s.lastReservedIP = ip
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) LastReservedIP() (net.IP, error) {
|
||||
return s.lastReservedIP, nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) Release(ip net.IP) error {
|
||||
delete(s.ipMap, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FakeStore) ReleaseByID(id string) error {
|
||||
toDelete := []string{}
|
||||
for k, v := range s.ipMap {
|
||||
if v == id {
|
||||
toDelete = append(toDelete, k)
|
||||
}
|
||||
}
|
||||
for _, ip := range toDelete {
|
||||
delete(s.ipMap, ip)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -60,7 +60,7 @@ func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) {
|
||||
}
|
||||
|
||||
if n.IPAM == nil {
|
||||
return nil, fmt.Errorf("%q missing 'ipam' key")
|
||||
return nil, fmt.Errorf("IPAM config missing 'ipam' key")
|
||||
}
|
||||
|
||||
// Copy net name into IPAM so not to drag Net struct around
|
||||
|
27
plugins/ipam/host-local/host_local_suite_test.go
Normal file
27
plugins/ipam/host-local/host_local_suite_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostLocal(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HostLocal Suite")
|
||||
}
|
2
test
2
test
@ -11,7 +11,7 @@ set -e
|
||||
|
||||
source ./build
|
||||
|
||||
TESTABLE="plugins/ipam/dhcp plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge"
|
||||
TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge"
|
||||
FORMATTABLE="$TESTABLE libcni pkg/ip pkg/ipam pkg/testutils plugins/ipam/host-local plugins/main/bridge plugins/meta/flannel plugins/meta/tuning"
|
||||
|
||||
# user has not provided PKG override
|
||||
|
Reference in New Issue
Block a user