ipam/host-local: Move allocator and config to backend

Signed-off-by: André Martins <aanm90@gmail.com>
This commit is contained in:
André Martins
2016-02-06 21:52:27 +00:00
parent 98ff61aac3
commit f60111b093
6 changed files with 13 additions and 12 deletions

View File

@ -0,0 +1,271 @@
// 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 allocator
import (
"fmt"
"log"
"net"
"github.com/containernetworking/cni/pkg/ip"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/plugins/ipam/host-local/backend"
)
type IPAllocator struct {
// start is inclusive and may be allocated
start net.IP
// end is inclusive and may be allocated
end net.IP
conf *IPAMConfig
store backend.Store
}
func NewIPAllocator(conf *IPAMConfig, store backend.Store) (*IPAllocator, error) {
// Can't create an allocator for a network with no addresses, eg
// a /32 or /31
ones, masklen := conf.Subnet.Mask.Size()
if ones > masklen-2 {
return nil, fmt.Errorf("Network %v too small to allocate from", conf.Subnet)
}
var (
start net.IP
end net.IP
err error
)
start, end, err = networkRange((*net.IPNet)(&conf.Subnet))
if err != nil {
return nil, err
}
// skip the .0 address
start = ip.NextIP(start)
if conf.RangeStart != nil {
if err := validateRangeIP(conf.RangeStart, (*net.IPNet)(&conf.Subnet), nil, nil); err != nil {
return nil, err
}
start = conf.RangeStart
}
if conf.RangeEnd != nil {
if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet), start, nil); err != nil {
return nil, err
}
end = conf.RangeEnd
}
return &IPAllocator{start, end, conf, store}, nil
}
func canonicalizeIP(ip net.IP) (net.IP, error) {
if ip.To4() != nil {
return ip.To4(), nil
} else if ip.To16() != nil {
return ip.To16(), nil
}
return nil, fmt.Errorf("IP %s not v4 nor v6", ip)
}
// Ensures @ip is within @ipnet, and (if given) inclusive of @start and @end
func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) error {
var err error
// Make sure we can compare IPv4 addresses directly
ip, err = canonicalizeIP(ip)
if err != nil {
return err
}
if !ipnet.Contains(ip) {
return fmt.Errorf("%s not in network: %s", ip, ipnet)
}
if start != nil {
start, err = canonicalizeIP(start)
if err != nil {
return err
}
if len(ip) != len(start) {
return fmt.Errorf("%s %d not same size IP address as start %s %d", ip, len(ip), start, len(start))
}
for i := 0; i < len(ip); i++ {
if ip[i] > start[i] {
break
} else if ip[i] < start[i] {
return fmt.Errorf("%s outside of network %s with start %s", ip, ipnet, start)
}
}
}
if end != nil {
end, err = canonicalizeIP(end)
if err != nil {
return err
}
if len(ip) != len(end) {
return fmt.Errorf("%s %d not same size IP address as end %s %d", ip, len(ip), end, len(end))
}
for i := 0; i < len(ip); i++ {
if ip[i] < end[i] {
break
} else if ip[i] > end[i] {
return fmt.Errorf("%s outside of network %s with end %s", ip, ipnet, end)
}
}
}
return nil
}
// Returns newly allocated IP along with its config
func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
a.store.Lock()
defer a.store.Unlock()
gw := a.conf.Gateway
if gw == nil {
gw = ip.NextIP(a.conf.Subnet.IP)
}
var requestedIP net.IP
if a.conf.Args != nil {
requestedIP = a.conf.Args.IP
}
if requestedIP != nil {
if gw != nil && gw.Equal(a.conf.Args.IP) {
return nil, fmt.Errorf("requested IP must differ gateway IP")
}
subnet := net.IPNet{
IP: a.conf.Subnet.IP,
Mask: a.conf.Subnet.Mask,
}
err := validateRangeIP(requestedIP, &subnet, a.start, a.end)
if err != nil {
return nil, err
}
reserved, err := a.store.Reserve(id, requestedIP)
if err != nil {
return nil, err
}
if reserved {
return &types.IPConfig{
IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
Gateway: gw,
Routes: a.conf.Routes,
}, nil
}
return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
}
startIP, endIP := a.getSearchRange()
for cur := startIP; ; cur = a.nextIP(cur) {
// don't allocate gateway IP
if gw != nil && cur.Equal(gw) {
continue
}
reserved, err := a.store.Reserve(id, cur)
if err != nil {
return nil, err
}
if reserved {
return &types.IPConfig{
IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
Gateway: gw,
Routes: a.conf.Routes,
}, nil
}
// break here to complete the loop
if cur.Equal(endIP) {
break
}
}
return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
}
// Releases all IPs allocated for the container with given ID
func (a *IPAllocator) Release(id string) error {
a.store.Lock()
defer a.store.Unlock()
return a.store.ReleaseByID(id)
}
// Return the start and end IP addresses of a given subnet, excluding
// the broadcast address (eg, 192.168.1.255)
func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) {
if ipnet.IP == nil {
return nil, nil, fmt.Errorf("missing field %q in IPAM configuration", "subnet")
}
ip, err := canonicalizeIP(ipnet.IP)
if err != nil {
return nil, nil, fmt.Errorf("IP not v4 nor v6")
}
if len(ip) != len(ipnet.Mask) {
return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch")
}
var end net.IP
for i := 0; i < len(ip); i++ {
end = append(end, ip[i]|^ipnet.Mask[i])
}
// Exclude the broadcast address for IPv4
if ip.To4() != nil {
end[3]--
}
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, a.start, a.end)
if err == nil {
startFromLastReservedIP = true
}
}
if startFromLastReservedIP {
startIP = a.nextIP(lastReservedIP)
endIP = lastReservedIP
} else {
startIP = a.start
endIP = a.end
}
return startIP, endIP
}

View 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 allocator_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestAllocator(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Allocator Suite")
}

View File

@ -0,0 +1,355 @@
// 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 allocator
import (
"fmt"
"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)
if err != nil {
return nil, err
}
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, err := NewIPAllocator(&conf, store)
if err != nil {
return nil, err
}
res, err := alloc.Get("ID")
if err != nil {
return nil, err
}
return res, nil
}
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/30",
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.6",
lastIP: "10.0.0.5",
},
{
subnet: "10.0.0.0/29",
ipmap: map[string]string{
"10.0.0.4": "id",
"10.0.0.5": "id",
},
expectResult: "10.0.0.6",
lastIP: "10.0.0.3",
},
// round robin to the beginning
{
subnet: "10.0.0.0/29",
ipmap: map[string]string{
"10.0.0.6": "id",
},
expectResult: "10.0.0.2",
lastIP: "10.0.0.5",
},
// 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",
},
// wrap around and reserve lastIP
{
subnet: "10.0.0.0/29",
ipmap: map[string]string{
"10.0.0.2": "id",
"10.0.0.4": "id",
"10.0.0.5": "id",
"10.0.0.6": "id",
},
expectResult: "10.0.0.3",
lastIP: "10.0.0.3",
},
}
for _, tc := range testCases {
res, err := tc.run()
Expect(err).ToNot(HaveOccurred())
Expect(res.IP.IP.String()).To(Equal(tc.expectResult))
}
})
It("should not allocate the broadcast address", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
for i := 1; i < 254; i++ {
res, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped
s := fmt.Sprintf("192.168.1.%d/24", i+1)
Expect(s).To(Equal(res.IP.String()))
}
_, err = alloc.Get("ID")
Expect(err).To(HaveOccurred())
})
It("should allocate RangeStart first", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP("192.168.1.10"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
res, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
Expect(res.IP.String()).To(Equal("192.168.1.10/24"))
res, err = alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
Expect(res.IP.String()).To(Equal("192.168.1.11/24"))
})
It("should allocate RangeEnd but not past RangeEnd", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeEnd: net.ParseIP("192.168.1.5"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred())
for i := 1; i < 5; i++ {
res, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped
Expect(res.IP.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1)))
}
_, err = alloc.Get("ID")
Expect(err).To(HaveOccurred())
})
Context("when requesting a specific IP", func() {
It("must allocate the requested IP", func() {
subnet, err := types.ParseCIDR("10.0.0.0/29")
Expect(err).ToNot(HaveOccurred())
requestedIP := net.ParseIP("10.0.0.2")
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: requestedIP},
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
res, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
Expect(res.IP.IP.String()).To(Equal(requestedIP.String()))
})
It("must return an error when the requested IP is after RangeEnd", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: net.ParseIP("192.168.1.50")},
RangeEnd: net.ParseIP("192.168.1.20"),
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
_, err = alloc.Get("ID")
Expect(err).To(HaveOccurred())
})
It("must return an error when the requested IP is before RangeStart", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
ipmap := map[string]string{}
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
Args: &IPAMArgs{IP: net.ParseIP("192.168.1.3")},
RangeStart: net.ParseIP("192.168.1.10"),
}
store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store)
_, err = alloc.Get("ID")
Expect(err).To(HaveOccurred())
})
})
It("RangeStart must be in the given subnet", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP("10.0.0.1"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
It("RangeEnd must be in the given subnet", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeEnd: net.ParseIP("10.0.0.1"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
It("RangeEnd must be after RangeStart in the given subnet", func() {
subnet, err := types.ParseCIDR("192.168.1.0/24")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP("192.168.1.10"),
RangeEnd: net.ParseIP("192.168.1.3"),
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
})
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"))
}
})
})
Context("when given an invalid subnet", func() {
It("returns a meaningful error", func() {
subnet, err := types.ParseCIDR("192.168.1.0/31")
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: "test",
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
}
store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP(""))
_, err = NewIPAllocator(&conf, store)
Expect(err).To(HaveOccurred())
})
})
})

View File

@ -0,0 +1,71 @@
// 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 allocator
import (
"encoding/json"
"fmt"
"net"
"github.com/containernetworking/cni/pkg/types"
)
// IPAMConfig represents the IP related network configuration.
type IPAMConfig struct {
Name string
Type string `json:"type"`
RangeStart net.IP `json:"rangeStart"`
RangeEnd net.IP `json:"rangeEnd"`
Subnet types.IPNet `json:"subnet"`
Gateway net.IP `json:"gateway"`
Routes []types.Route `json:"routes"`
DataDir string `json:"dataDir"`
Args *IPAMArgs `json:"-"`
}
type IPAMArgs struct {
types.CommonArgs
IP net.IP `json:"ip,omitempty"`
}
type Net struct {
Name string `json:"name"`
IPAM *IPAMConfig `json:"ipam"`
}
// NewIPAMConfig creates a NetworkConfig from the given network name.
func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) {
n := Net{}
if err := json.Unmarshal(bytes, &n); err != nil {
return nil, err
}
if args != "" {
n.IPAM.Args = &IPAMArgs{}
err := types.LoadArgs(args, n.IPAM.Args)
if err != nil {
return nil, err
}
}
if n.IPAM == nil {
return nil, fmt.Errorf("IPAM config missing 'ipam' key")
}
// Copy net name into IPAM so not to drag Net struct around
n.IPAM.Name = n.Name
return n.IPAM, nil
}