plugins: add plugins from containernetworking/cni

Plugins prepared from the containernetworking/cni repo as follows:

1) git reset --hard 1a9288c3c0
2) git remove everything not in plugins/
3) git remove plugins/test
4) git merge into containernetworking/plugins repo
5) adjust import paths for containernetworking/cni -> containernetworking/plugins
This commit is contained in:
Dan Williams
2017-05-14 23:06:45 -05:00
39 changed files with 5604 additions and 21 deletions

159
plugins/ipam/dhcp/daemon.go Normal file
View File

@ -0,0 +1,159 @@
// 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 main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/coreos/go-systemd/activation"
)
const listenFdsStart = 3
const resendCount = 3
var errNoMoreTries = errors.New("no more tries")
type DHCP struct {
mux sync.Mutex
leases map[string]*DHCPLease
}
func newDHCP() *DHCP {
return &DHCP{
leases: make(map[string]*DHCPLease),
}
}
// Allocate acquires an IP from a DHCP server for a specified container.
// The acquired lease will be maintained until Release() is called.
func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
conf := types.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("error parsing netconf: %v", err)
}
clientID := args.ContainerID + "/" + conf.Name
l, err := AcquireLease(clientID, args.Netns, args.IfName)
if err != nil {
return err
}
ipn, err := l.IPNet()
if err != nil {
l.Stop()
return err
}
d.setLease(args.ContainerID, conf.Name, l)
result.IPs = []*current.IPConfig{{
Version: "4",
Address: *ipn,
Gateway: l.Gateway(),
}}
result.Routes = l.Routes()
return nil
}
// Release stops maintenance of the lease acquired in Allocate()
// and sends a release msg to the DHCP server.
func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
conf := types.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("error parsing netconf: %v", err)
}
if l := d.getLease(args.ContainerID, conf.Name); l != nil {
l.Stop()
return nil
}
return fmt.Errorf("lease not found: %v/%v", args.ContainerID, conf.Name)
}
func (d *DHCP) getLease(contID, netName string) *DHCPLease {
d.mux.Lock()
defer d.mux.Unlock()
// TODO(eyakubovich): hash it to avoid collisions
l, ok := d.leases[contID+netName]
if !ok {
return nil
}
return l
}
func (d *DHCP) setLease(contID, netName string, l *DHCPLease) {
d.mux.Lock()
defer d.mux.Unlock()
// TODO(eyakubovich): hash it to avoid collisions
d.leases[contID+netName] = l
}
func getListener() (net.Listener, error) {
l, err := activation.Listeners(true)
if err != nil {
return nil, err
}
switch {
case len(l) == 0:
if err := os.MkdirAll(filepath.Dir(socketPath), 0700); err != nil {
return nil, err
}
return net.Listen("unix", socketPath)
case len(l) == 1:
if l[0] == nil {
return nil, fmt.Errorf("LISTEN_FDS=1 but no FD found")
}
return l[0], nil
default:
return nil, fmt.Errorf("Too many (%v) FDs passed through socket activation", len(l))
}
}
func runDaemon() {
// since other goroutines (on separate threads) will change namespaces,
// ensure the RPC server does not get scheduled onto those
runtime.LockOSThread()
l, err := getListener()
if err != nil {
log.Printf("Error getting listener: %v", err)
return
}
dhcp := newDHCP()
rpc.Register(dhcp)
rpc.HandleHTTP()
http.Serve(l, nil)
}

337
plugins/ipam/dhcp/lease.go Normal file
View File

@ -0,0 +1,337 @@
// 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 main
import (
"fmt"
"log"
"math/rand"
"net"
"sync"
"time"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
"github.com/vishvananda/netlink"
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/types"
)
// RFC 2131 suggests using exponential backoff, starting with 4sec
// and randomized to +/- 1sec
const resendDelay0 = 4 * time.Second
const resendDelayMax = 32 * time.Second
const (
leaseStateBound = iota
leaseStateRenewing
leaseStateRebinding
)
// This implementation uses 1 OS thread per lease. This is because
// all the network operations have to be done in network namespace
// of the interface. This can be improved by switching to the proper
// namespace for network ops and using fewer threads. However, this
// needs to be done carefully as dhcp4client ops are blocking.
type DHCPLease struct {
clientID string
ack *dhcp4.Packet
opts dhcp4.Options
link netlink.Link
renewalTime time.Time
rebindingTime time.Time
expireTime time.Time
stop chan struct{}
wg sync.WaitGroup
}
// AcquireLease gets an DHCP lease and then maintains it in the background
// by periodically renewing it. The acquired lease can be released by
// calling DHCPLease.Stop()
func AcquireLease(clientID, netns, ifName string) (*DHCPLease, error) {
errCh := make(chan error, 1)
l := &DHCPLease{
clientID: clientID,
stop: make(chan struct{}),
}
log.Printf("%v: acquiring lease", clientID)
l.wg.Add(1)
go func() {
errCh <- ns.WithNetNSPath(netns, func(_ ns.NetNS) error {
defer l.wg.Done()
link, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("error looking up %q: %v", ifName, err)
}
l.link = link
if err = l.acquire(); err != nil {
return err
}
log.Printf("%v: lease acquired, expiration is %v", l.clientID, l.expireTime)
errCh <- nil
l.maintain()
return nil
})
}()
if err := <-errCh; err != nil {
return nil, err
}
return l, nil
}
// Stop terminates the background task that maintains the lease
// and issues a DHCP Release
func (l *DHCPLease) Stop() {
close(l.stop)
l.wg.Wait()
}
func (l *DHCPLease) acquire() error {
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
if (l.link.Attrs().Flags & net.FlagUp) != net.FlagUp {
log.Printf("Link %q down. Attempting to set up", l.link.Attrs().Name)
if err = netlink.LinkSetUp(l.link); err != nil {
return err
}
}
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
ok, ack, err := c.Request()
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server NACK'd own offer")
default:
return &ack, nil
}
})
if err != nil {
return err
}
return l.commit(pkt)
}
func (l *DHCPLease) commit(ack *dhcp4.Packet) error {
opts := ack.ParseOptions()
leaseTime, err := parseLeaseTime(opts)
if err != nil {
return err
}
rebindingTime, err := parseRebindingTime(opts)
if err != nil || rebindingTime > leaseTime {
// Per RFC 2131 Section 4.4.5, it should default to 85% of lease time
rebindingTime = leaseTime * 85 / 100
}
renewalTime, err := parseRenewalTime(opts)
if err != nil || renewalTime > rebindingTime {
// Per RFC 2131 Section 4.4.5, it should default to 50% of lease time
renewalTime = leaseTime / 2
}
now := time.Now()
l.expireTime = now.Add(leaseTime)
l.renewalTime = now.Add(renewalTime)
l.rebindingTime = now.Add(rebindingTime)
l.ack = ack
l.opts = opts
return nil
}
func (l *DHCPLease) maintain() {
state := leaseStateBound
for {
var sleepDur time.Duration
switch state {
case leaseStateBound:
sleepDur = l.renewalTime.Sub(time.Now())
if sleepDur <= 0 {
log.Printf("%v: renewing lease", l.clientID)
state = leaseStateRenewing
continue
}
case leaseStateRenewing:
if err := l.renew(); err != nil {
log.Printf("%v: %v", l.clientID, err)
if time.Now().After(l.rebindingTime) {
log.Printf("%v: renawal time expired, rebinding", l.clientID)
state = leaseStateRebinding
}
} else {
log.Printf("%v: lease renewed, expiration is %v", l.clientID, l.expireTime)
state = leaseStateBound
}
case leaseStateRebinding:
if err := l.acquire(); err != nil {
log.Printf("%v: %v", l.clientID, err)
if time.Now().After(l.expireTime) {
log.Printf("%v: lease expired, bringing interface DOWN", l.clientID)
l.downIface()
return
}
} else {
log.Printf("%v: lease rebound, expiration is %v", l.clientID, l.expireTime)
state = leaseStateBound
}
}
select {
case <-time.After(sleepDur):
case <-l.stop:
if err := l.release(); err != nil {
log.Printf("%v: failed to release DHCP lease: %v", l.clientID, err)
}
return
}
}
}
func (l *DHCPLease) downIface() {
if err := netlink.LinkSetDown(l.link); err != nil {
log.Printf("%v: failed to bring %v interface DOWN: %v", l.clientID, l.link.Attrs().Name, err)
}
}
func (l *DHCPLease) renew() error {
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
ok, ack, err := c.Renew(*l.ack)
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server did not renew lease")
default:
return &ack, nil
}
})
if err != nil {
return err
}
l.commit(pkt)
return nil
}
func (l *DHCPLease) release() error {
log.Printf("%v: releasing lease", l.clientID)
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
if err = c.Release(*l.ack); err != nil {
return fmt.Errorf("failed to send DHCPRELEASE")
}
return nil
}
func (l *DHCPLease) IPNet() (*net.IPNet, error) {
mask := parseSubnetMask(l.opts)
if mask == nil {
return nil, fmt.Errorf("DHCP option Subnet Mask not found in DHCPACK")
}
return &net.IPNet{
IP: l.ack.YIAddr(),
Mask: mask,
}, nil
}
func (l *DHCPLease) Gateway() net.IP {
return parseRouter(l.opts)
}
func (l *DHCPLease) Routes() []*types.Route {
routes := parseRoutes(l.opts)
return append(routes, parseCIDRRoutes(l.opts)...)
}
// jitter returns a random value within [-span, span) range
func jitter(span time.Duration) time.Duration {
return time.Duration(float64(span) * (2.0*rand.Float64() - 1.0))
}
func backoffRetry(f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
var baseDelay time.Duration = resendDelay0
for i := 0; i < resendCount; i++ {
pkt, err := f()
if err == nil {
return pkt, nil
}
log.Print(err)
time.Sleep(baseDelay + jitter(time.Second))
if baseDelay < resendDelayMax {
baseDelay *= 2
}
}
return nil, errNoMoreTries
}
func newDHCPClient(link netlink.Link) (*dhcp4client.Client, error) {
pktsock, err := dhcp4client.NewPacketSock(link.Attrs().Index)
if err != nil {
return nil, err
}
return dhcp4client.New(
dhcp4client.HardwareAddr(link.Attrs().HardwareAddr),
dhcp4client.Timeout(5*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(pktsock),
)
}

83
plugins/ipam/dhcp/main.go Normal file
View File

@ -0,0 +1,83 @@
// 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 main
import (
"fmt"
"net/rpc"
"os"
"path/filepath"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
)
const socketPath = "/run/cni/dhcp.sock"
func main() {
if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon()
} else {
skel.PluginMain(cmdAdd, cmdDel, version.All)
}
}
func cmdAdd(args *skel.CmdArgs) error {
// Plugin must return result in same version as specified in netconf
versionDecoder := &version.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}
result := &current.Result{}
if err := rpcCall("DHCP.Allocate", args, result); err != nil {
return err
}
return types.PrintResult(result, confVersion)
}
func cmdDel(args *skel.CmdArgs) error {
result := struct{}{}
if err := rpcCall("DHCP.Release", args, &result); err != nil {
return fmt.Errorf("error dialing DHCP daemon: %v", err)
}
return nil
}
func rpcCall(method string, args *skel.CmdArgs, result interface{}) error {
client, err := rpc.DialHTTP("unix", socketPath)
if err != nil {
return fmt.Errorf("error dialing DHCP daemon: %v", err)
}
// The daemon may be running under a different working dir
// so make sure the netns path is absolute.
netns, err := filepath.Abs(args.Netns)
if err != nil {
return fmt.Errorf("failed to make %q an absolute path: %v", args.Netns, err)
}
args.Netns = netns
err = client.Call(method, args, result)
if err != nil {
return fmt.Errorf("error calling %v: %v", method, err)
}
return nil
}

View File

@ -0,0 +1,139 @@
// 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 main
import (
"encoding/binary"
"fmt"
"net"
"time"
"github.com/containernetworking/cni/pkg/types"
"github.com/d2g/dhcp4"
)
func parseRouter(opts dhcp4.Options) net.IP {
if opts, ok := opts[dhcp4.OptionRouter]; ok {
if len(opts) == 4 {
return net.IP(opts)
}
}
return nil
}
func classfulSubnet(sn net.IP) net.IPNet {
return net.IPNet{
IP: sn,
Mask: sn.DefaultMask(),
}
}
func parseRoutes(opts dhcp4.Options) []*types.Route {
// StaticRoutes format: pairs of:
// Dest = 4 bytes; Classful IP subnet
// Router = 4 bytes; IP address of router
routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionStaticRoute]; ok {
for len(opt) >= 8 {
sn := opt[0:4]
r := opt[4:8]
rt := &types.Route{
Dst: classfulSubnet(sn),
GW: r,
}
routes = append(routes, rt)
opt = opt[8:]
}
}
return routes
}
func parseCIDRRoutes(opts dhcp4.Options) []*types.Route {
// See RFC4332 for format (http://tools.ietf.org/html/rfc3442)
routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok {
for len(opt) >= 5 {
width := int(opt[0])
if width > 32 {
// error: can't have more than /32
return nil
}
// network bits are compacted to avoid zeros
octets := 0
if width > 0 {
octets = (width-1)/8 + 1
}
if len(opt) < 1+octets+4 {
// error: too short
return nil
}
sn := make([]byte, 4)
copy(sn, opt[1:octets+1])
gw := net.IP(opt[octets+1 : octets+5])
rt := &types.Route{
Dst: net.IPNet{
IP: net.IP(sn),
Mask: net.CIDRMask(width, 32),
},
GW: gw,
}
routes = append(routes, rt)
opt = opt[octets+5 : len(opt)]
}
}
return routes
}
func parseSubnetMask(opts dhcp4.Options) net.IPMask {
mask, ok := opts[dhcp4.OptionSubnetMask]
if !ok {
return nil
}
return net.IPMask(mask)
}
func parseDuration(opts dhcp4.Options, code dhcp4.OptionCode, optName string) (time.Duration, error) {
val, ok := opts[code]
if !ok {
return 0, fmt.Errorf("option %v not found", optName)
}
if len(val) != 4 {
return 0, fmt.Errorf("option %v is not 4 bytes", optName)
}
secs := binary.BigEndian.Uint32(val)
return time.Duration(secs) * time.Second, nil
}
func parseLeaseTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionIPAddressLeaseTime, "LeaseTime")
}
func parseRenewalTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRenewalTimeValue, "RenewalTime")
}
func parseRebindingTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRebindingTimeValue, "RebindingTime")
}

View File

@ -0,0 +1,75 @@
// 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 main
import (
"net"
"testing"
"github.com/containernetworking/cni/pkg/types"
"github.com/d2g/dhcp4"
)
func validateRoutes(t *testing.T, routes []*types.Route) {
expected := []*types.Route{
&types.Route{
Dst: net.IPNet{
IP: net.IPv4(10, 0, 0, 0),
Mask: net.CIDRMask(8, 32),
},
GW: net.IPv4(10, 1, 2, 3),
},
&types.Route{
Dst: net.IPNet{
IP: net.IPv4(192, 168, 1, 0),
Mask: net.CIDRMask(24, 32),
},
GW: net.IPv4(192, 168, 2, 3),
},
}
if len(routes) != len(expected) {
t.Fatalf("wrong length slice; expected %v, got %v", len(expected), len(routes))
}
for i := 0; i < len(routes); i++ {
a := routes[i]
e := expected[i]
if a.Dst.String() != e.Dst.String() {
t.Errorf("route.Dst mismatch: expected %v, got %v", e.Dst, a.Dst)
}
if !a.GW.Equal(e.GW) {
t.Errorf("route.GW mismatch: expected %v, got %v", e.GW, a.GW)
}
}
}
func TestParseRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionStaticRoute] = []byte{10, 0, 0, 0, 10, 1, 2, 3, 192, 168, 1, 0, 192, 168, 2, 3}
routes := parseRoutes(opts)
validateRoutes(t, routes)
}
func TestParseCIDRRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionClasslessRouteFormat] = []byte{8, 10, 10, 1, 2, 3, 24, 192, 168, 1, 192, 168, 2, 3}
routes := parseCIDRRoutes(opts)
validateRoutes(t, routes)
}

View File

@ -0,0 +1,277 @@
// 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"
"os"
"github.com/containernetworking/cni/pkg/ip"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/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), start, end); err != nil {
return nil, err
}
start = conf.RangeStart
}
if conf.RangeEnd != nil {
if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet), start, end); 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) (*current.IPConfig, []*types.Route, 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, 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, nil, err
}
reserved, err := a.store.Reserve(id, requestedIP)
if err != nil {
return nil, nil, err
}
if reserved {
ipConfig := &current.IPConfig{
Version: "4",
Address: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
Gateway: gw,
}
routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
}
return nil, 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, nil, err
}
if reserved {
ipConfig := &current.IPConfig{
Version: "4",
Address: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
Gateway: gw,
}
routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
}
// break here to complete the loop
if cur.Equal(endIP) {
break
}
}
return nil, 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 && !os.IsNotExist(err) {
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,378 @@
// 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"
"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"
"net"
)
type AllocatorTestCase struct {
subnet string
ipmap map[string]string
expectResult string
lastIP string
}
func (t AllocatorTestCase) run() (*current.IPConfig, []*types.Route, error) {
subnet, err := types.ParseCIDR(t.subnet)
if err != nil {
return nil, 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, nil, err
}
res, routes, err := alloc.Get("ID")
if err != nil {
return nil, nil, err
}
return res, routes, 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.Address.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.Address.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.Address.String()).To(Equal("192.168.1.10/24"))
res, _, err = alloc.Get("ID")
Expect(err).ToNot(HaveOccurred())
Expect(res.Address.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.Address.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.Address.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() {
testcases := []struct {
name string
ipnet string
start string
}{
{"outside-subnet", "192.168.1.0/24", "10.0.0.1"},
{"zero-ip", "10.1.0.0/16", "10.1.0.0"},
}
for _, tc := range testcases {
subnet, err := types.ParseCIDR(tc.ipnet)
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: tc.name,
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeStart: net.ParseIP(tc.start),
}
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() {
testcases := []struct {
name string
ipnet string
end string
}{
{"outside-subnet", "192.168.1.0/24", "10.0.0.1"},
{"broadcast-ip", "10.1.0.0/16", "10.1.255.255"},
}
for _, tc := range testcases {
subnet, err := types.ParseCIDR(tc.ipnet)
Expect(err).ToNot(HaveOccurred())
conf := IPAMConfig{
Name: tc.name,
Type: "host-local",
Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask},
RangeEnd: net.ParseIP(tc.end),
}
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,84 @@
// 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"`
ResolvConf string `json:"resolvConf"`
Args *IPAMArgs `json:"-"`
}
type IPAMArgs struct {
types.CommonArgs
IP net.IP `json:"ip,omitempty"`
}
type Net struct {
Name string `json:"name"`
CNIVersion string `json:"cniVersion"`
IPAM *IPAMConfig `json:"ipam"`
}
// NewIPAMConfig creates a NetworkConfig from the given network name.
func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) {
n := Net{}
if err := json.Unmarshal(bytes, &n); err != nil {
return nil, "", err
}
if n.IPAM == nil {
return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
}
if args != "" {
n.IPAM.Args = &IPAMArgs{}
err := types.LoadArgs(args, n.IPAM.Args)
if err != nil {
return nil, "", err
}
}
// Copy net name into IPAM so not to drag Net struct around
n.IPAM.Name = n.Name
return n.IPAM, n.CNIVersion, nil
}
func convertRoutesToCurrent(routes []types.Route) []*types.Route {
var currentRoutes []*types.Route
for _, r := range routes {
currentRoutes = append(currentRoutes, &types.Route{
Dst: r.Dst,
GW: r.GW,
})
}
return currentRoutes
}

View File

@ -0,0 +1,115 @@
// 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 disk
import (
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
)
const lastIPFile = "last_reserved_ip"
var defaultDataDir = "/var/lib/cni/networks"
type Store struct {
FileLock
dataDir string
}
// Store implements the Store interface
var _ backend.Store = &Store{}
func New(network, dataDir string) (*Store, error) {
if dataDir == "" {
dataDir = defaultDataDir
}
dir := filepath.Join(dataDir, network)
if err := os.MkdirAll(dir, 0644); err != nil {
return nil, err
}
lk, err := NewFileLock(dir)
if err != nil {
return nil, err
}
return &Store{*lk, dir}, nil
}
func (s *Store) Reserve(id string, ip net.IP) (bool, error) {
fname := filepath.Join(s.dataDir, ip.String())
f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
if os.IsExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if _, err := f.WriteString(strings.TrimSpace(id)); err != nil {
f.Close()
os.Remove(f.Name())
return false, err
}
if err := f.Close(); err != nil {
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, err
}
return net.ParseIP(string(data)), nil
}
func (s *Store) Release(ip net.IP) error {
return os.Remove(filepath.Join(s.dataDir, ip.String()))
}
// N.B. This function eats errors to be tolerant and
// release as much as possible
func (s *Store) ReleaseByID(id string) error {
err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil
}
if strings.TrimSpace(string(data)) == strings.TrimSpace(id) {
if err := os.Remove(path); err != nil {
return nil
}
}
return nil
})
return err
}

View File

@ -0,0 +1,50 @@
// 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 disk
import (
"os"
"syscall"
)
// FileLock wraps os.File to be used as a lock using flock
type FileLock struct {
f *os.File
}
// NewFileLock opens file/dir at path and returns unlocked FileLock object
func NewFileLock(path string) (*FileLock, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
return &FileLock{f}, nil
}
// Close closes underlying file
func (l *FileLock) Close() error {
return l.f.Close()
}
// Lock acquires an exclusive lock
func (l *FileLock) Lock() error {
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX)
}
// Unlock releases the lock
func (l *FileLock) Unlock() error {
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
}

View File

@ -0,0 +1,27 @@
// 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 backend
import "net"
type Store interface {
Lock() error
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
}

View File

@ -0,0 +1,77 @@
// 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"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
)
type FakeStore struct {
ipMap map[string]string
lastReservedIP net.IP
}
// FakeStore implements the Store interface
var _ backend.Store = &FakeStore{}
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
}

View File

@ -0,0 +1,64 @@
// 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 (
"bufio"
"os"
"strings"
"github.com/containernetworking/cni/pkg/types"
)
// parseResolvConf parses an existing resolv.conf in to a DNS struct
func parseResolvConf(filename string) (*types.DNS, error) {
fp, err := os.Open(filename)
if err != nil {
return nil, err
}
dns := types.DNS{}
scanner := bufio.NewScanner(fp)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
// Skip comments, empty lines
if len(line) == 0 || line[0] == '#' || line[0] == ';' {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
switch fields[0] {
case "nameserver":
dns.Nameservers = append(dns.Nameservers, fields[1])
case "domain":
dns.Domain = fields[1]
case "search":
dns.Search = append(dns.Search, fields[1:]...)
case "options":
dns.Options = append(dns.Options, fields[1:]...)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return &dns, nil
}

View File

@ -0,0 +1,80 @@
// 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 (
"io/ioutil"
"os"
"github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("parsing resolv.conf", func() {
It("parses a simple resolv.conf file", func() {
contents := `
nameserver 192.0.2.0
nameserver 192.0.2.1
`
dns, err := parse(contents)
Expect(err).NotTo(HaveOccurred())
Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0", "192.0.2.1"}}))
})
It("ignores comments", func() {
dns, err := parse(`
nameserver 192.0.2.0
;nameserver 192.0.2.1
`)
Expect(err).NotTo(HaveOccurred())
Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0"}}))
})
It("parses all fields", func() {
dns, err := parse(`
nameserver 192.0.2.0
nameserver 192.0.2.2
domain example.com
;nameserver comment
#nameserver comment
search example.net example.org
search example.gov
options one two three
options four
`)
Expect(err).NotTo(HaveOccurred())
Expect(*dns).Should(Equal(types.DNS{
Nameservers: []string{"192.0.2.0", "192.0.2.2"},
Domain: "example.com",
Search: []string{"example.net", "example.org", "example.gov"},
Options: []string{"one", "two", "three", "four"},
}))
})
})
func parse(contents string) (*types.DNS, error) {
f, err := ioutil.TempFile("", "host_local_resolv")
defer f.Close()
defer os.Remove(f.Name())
if err != nil {
return nil, err
}
if _, err := f.WriteString(contents); err != nil {
return nil, err
}
return parseResolvConf(f.Name())
}

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

View File

@ -0,0 +1,292 @@
// 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 (
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/testutils"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/current"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("host-local Operations", func() {
It("allocates and releases an address with ADD/DEL", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
Expect(err).NotTo(HaveOccurred())
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",
"resolvConf": "%s/resolv.conf"
}
}`, tmpDir, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
Expect(err).NotTo(HaveOccurred())
Expect(len(result.IPs)).To(Equal(1))
expectedAddress.IP = expectedAddress.IP.To16()
Expect(result.IPs[0].Address).To(Equal(*expectedAddress))
Expect(result.IPs[0].Gateway).To(Equal(net.ParseIP("10.1.2.1")))
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
contents, err := ioutil.ReadFile(ipFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
contents, err = ioutil.ReadFile(lastFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(ipFilePath)
Expect(err).To(HaveOccurred())
})
It("doesn't error when passed an unknown ID on DEL", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
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"
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
})
It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
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"
}
}`, tmpDir, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
result, err := types020.GetResult(r)
Expect(err).NotTo(HaveOccurred())
expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
Expect(err).NotTo(HaveOccurred())
expectedAddress.IP = expectedAddress.IP.To16()
Expect(result.IP4.IP).To(Equal(*expectedAddress))
Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1")))
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
contents, err := ioutil.ReadFile(ipFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
contents, err = ioutil.ReadFile(lastFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
Expect(result.DNS).To(Equal(types.DNS{Nameservers: []string{"192.0.2.3"}}))
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(ipFilePath)
Expect(err).To(HaveOccurred())
})
It("ignores whitespace in disk files", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
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"
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: " dummy\n ",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
ipFilePath := filepath.Join(tmpDir, "mynet", result.IPs[0].Address.IP.String())
contents, err := ioutil.ReadFile(ipFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(ipFilePath)
Expect(err).To(HaveOccurred())
})
It("does not output an error message upon initial subnet creation", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
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"
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "testing",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
_, out, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(out), "Error retriving last reserved ip")).To(Equal(-1))
})
})

View File

@ -0,0 +1,86 @@
// 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 main
import (
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator"
"github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
)
func main() {
skel.PluginMain(cmdAdd, cmdDel, version.All)
}
func cmdAdd(args *skel.CmdArgs) error {
ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
if err != nil {
return err
}
result := &current.Result{}
if ipamConf.ResolvConf != "" {
dns, err := parseResolvConf(ipamConf.ResolvConf)
if err != nil {
return err
}
result.DNS = *dns
}
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
if err != nil {
return err
}
defer store.Close()
allocator, err := allocator.NewIPAllocator(ipamConf, store)
if err != nil {
return err
}
ipConf, routes, err := allocator.Get(args.ContainerID)
if err != nil {
return err
}
result.IPs = []*current.IPConfig{ipConf}
result.Routes = routes
return types.PrintResult(result, confVersion)
}
func cmdDel(args *skel.CmdArgs) error {
ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
if err != nil {
return err
}
store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
if err != nil {
return err
}
defer store.Close()
ipAllocator, err := allocator.NewIPAllocator(ipamConf, store)
if err != nil {
return err
}
return ipAllocator.Release(args.ContainerID)
}