diff --git a/plugins/ipam/host-local/allocator.go b/plugins/ipam/host-local/allocator.go index 55a3ae6f..3610646b 100644 --- a/plugins/ipam/host-local/allocator.go +++ b/plugins/ipam/host-local/allocator.go @@ -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 +} diff --git a/plugins/ipam/host-local/allocator_test.go b/plugins/ipam/host-local/allocator_test.go new file mode 100644 index 00000000..b0402af6 --- /dev/null +++ b/plugins/ipam/host-local/allocator_test.go @@ -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")) + } + }) + }) +}) diff --git a/plugins/ipam/host-local/backend/disk/backend.go b/plugins/ipam/host-local/backend/disk/backend.go index 88dc5e92..ab4ddb4e 100644 --- a/plugins/ipam/host-local/backend/disk/backend.go +++ b/plugins/ipam/host-local/backend/disk/backend.go @@ -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())) } diff --git a/plugins/ipam/host-local/backend/store.go b/plugins/ipam/host-local/backend/store.go index 45a89b10..82ba8693 100644 --- a/plugins/ipam/host-local/backend/store.go +++ b/plugins/ipam/host-local/backend/store.go @@ -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 } diff --git a/plugins/ipam/host-local/backend/testing/fake_store.go b/plugins/ipam/host-local/backend/testing/fake_store.go new file mode 100644 index 00000000..f7750cac --- /dev/null +++ b/plugins/ipam/host-local/backend/testing/fake_store.go @@ -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 +} diff --git a/plugins/ipam/host-local/config.go b/plugins/ipam/host-local/config.go index a0e493cd..faef3360 100644 --- a/plugins/ipam/host-local/config.go +++ b/plugins/ipam/host-local/config.go @@ -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 diff --git a/plugins/ipam/host-local/host_local_suite_test.go b/plugins/ipam/host-local/host_local_suite_test.go new file mode 100644 index 00000000..4dc89a03 --- /dev/null +++ b/plugins/ipam/host-local/host_local_suite_test.go @@ -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") +} diff --git a/test b/test index 6db93b71..5ac4cda3 100755 --- a/test +++ b/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