plugins: add plugins from containernetworking/cni
Plugins prepared from the containernetworking/cni repo as follows: 1) git reset --hard 1a9288c3c09cea4e580fdb1a636f1c5e185a391f 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:
commit
d2792f264e
2
build
2
build
@ -15,7 +15,7 @@ export GOPATH=${PWD}/gopath
|
|||||||
mkdir -p "${PWD}/bin"
|
mkdir -p "${PWD}/bin"
|
||||||
|
|
||||||
echo "Building plugins"
|
echo "Building plugins"
|
||||||
PLUGINS="plugins/sample plugins/main/vlan"
|
PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample"
|
||||||
for d in $PLUGINS; do
|
for d in $PLUGINS; do
|
||||||
if [ -d "$d" ]; then
|
if [ -d "$d" ]; then
|
||||||
plugin="$(basename "$d")"
|
plugin="$(basename "$d")"
|
||||||
|
159
plugins/ipam/dhcp/daemon.go
Normal file
159
plugins/ipam/dhcp/daemon.go
Normal 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
337
plugins/ipam/dhcp/lease.go
Normal 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
83
plugins/ipam/dhcp/main.go
Normal 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 := ¤t.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
|
||||||
|
}
|
139
plugins/ipam/dhcp/options.go
Normal file
139
plugins/ipam/dhcp/options.go
Normal 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")
|
||||||
|
}
|
75
plugins/ipam/dhcp/options_test.go
Normal file
75
plugins/ipam/dhcp/options_test.go
Normal 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)
|
||||||
|
}
|
277
plugins/ipam/host-local/backend/allocator/allocator.go
Normal file
277
plugins/ipam/host-local/backend/allocator/allocator.go
Normal 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 := ¤t.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 := ¤t.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
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
378
plugins/ipam/host-local/backend/allocator/allocator_test.go
Normal file
378
plugins/ipam/host-local/backend/allocator/allocator_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
84
plugins/ipam/host-local/backend/allocator/config.go
Normal file
84
plugins/ipam/host-local/backend/allocator/config.go
Normal 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
|
||||||
|
}
|
115
plugins/ipam/host-local/backend/disk/backend.go
Normal file
115
plugins/ipam/host-local/backend/disk/backend.go
Normal 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
|
||||||
|
}
|
50
plugins/ipam/host-local/backend/disk/lock.go
Normal file
50
plugins/ipam/host-local/backend/disk/lock.go
Normal 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)
|
||||||
|
}
|
27
plugins/ipam/host-local/backend/store.go
Normal file
27
plugins/ipam/host-local/backend/store.go
Normal 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
|
||||||
|
}
|
77
plugins/ipam/host-local/backend/testing/fake_store.go
Normal file
77
plugins/ipam/host-local/backend/testing/fake_store.go
Normal 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
|
||||||
|
}
|
64
plugins/ipam/host-local/dns.go
Normal file
64
plugins/ipam/host-local/dns.go
Normal 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
|
||||||
|
}
|
80
plugins/ipam/host-local/dns_test.go
Normal file
80
plugins/ipam/host-local/dns_test.go
Normal 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())
|
||||||
|
}
|
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
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHostLocal(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "HostLocal Suite")
|
||||||
|
}
|
292
plugins/ipam/host-local/host_local_test.go
Normal file
292
plugins/ipam/host-local/host_local_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
86
plugins/ipam/host-local/main.go
Normal file
86
plugins/ipam/host-local/main.go
Normal 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 := ¤t.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)
|
||||||
|
}
|
417
plugins/main/bridge/bridge.go
Normal file
417
plugins/main/bridge/bridge.go
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
// Copyright 2014 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"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ip"
|
||||||
|
"github.com/containernetworking/cni/pkg/ipam"
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/utils"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBrName = "cni0"
|
||||||
|
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
BrName string `json:"bridge"`
|
||||||
|
IsGW bool `json:"isGateway"`
|
||||||
|
IsDefaultGW bool `json:"isDefaultGateway"`
|
||||||
|
ForceAddress bool `json:"forceAddress"`
|
||||||
|
IPMasq bool `json:"ipMasq"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
HairpinMode bool `json:"hairpinMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// this ensures that main runs only on main thread (thread group leader).
|
||||||
|
// since namespace ops (unshare, setns) are done for a single thread, we
|
||||||
|
// must ensure that the goroutine does not jump from OS thread to thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNetConf(bytes []byte) (*NetConf, string, error) {
|
||||||
|
n := &NetConf{
|
||||||
|
BrName: defaultBrName,
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bytes, n); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
return n, n.CNIVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet, forceAddress bool) error {
|
||||||
|
addrs, err := netlink.AddrList(br, syscall.AF_INET)
|
||||||
|
if err != nil && err != syscall.ENOENT {
|
||||||
|
return fmt.Errorf("could not get list of IP addresses: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there're no addresses on the bridge, it's ok -- we'll add one
|
||||||
|
if len(addrs) > 0 {
|
||||||
|
ipnStr := ipn.String()
|
||||||
|
for _, a := range addrs {
|
||||||
|
// string comp is actually easiest for doing IPNet comps
|
||||||
|
if a.IPNet.String() == ipnStr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If forceAddress is set to true then reconfigure IP address otherwise throw error
|
||||||
|
if forceAddress {
|
||||||
|
if err = deleteBridgeAddr(br, a.IPNet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("%q already has an IP address different from %v", br.Name, ipn.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||||
|
if err := netlink.AddrAdd(br, addr); err != nil {
|
||||||
|
return fmt.Errorf("could not add IP address to %q: %v", br.Name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBridgeAddr(br *netlink.Bridge, ipn *net.IPNet) error {
|
||||||
|
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||||
|
|
||||||
|
if err := netlink.LinkSetDown(br); err != nil {
|
||||||
|
return fmt.Errorf("could not set down bridge %q: %v", br.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.AddrDel(br, addr); err != nil {
|
||||||
|
return fmt.Errorf("could not remove IP address from %q: %v", br.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.LinkSetUp(br); err != nil {
|
||||||
|
return fmt.Errorf("could not set up bridge %q: %v", br.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bridgeByName(name string) (*netlink.Bridge, error) {
|
||||||
|
l, err := netlink.LinkByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not lookup %q: %v", name, err)
|
||||||
|
}
|
||||||
|
br, ok := l.(*netlink.Bridge)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%q already exists but is not a bridge", name)
|
||||||
|
}
|
||||||
|
return br, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
|
||||||
|
br := &netlink.Bridge{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: brName,
|
||||||
|
MTU: mtu,
|
||||||
|
// Let kernel use default txqueuelen; leaving it unset
|
||||||
|
// means 0, and a zero-length TX queue messes up FIFO
|
||||||
|
// traffic shapers which use TX queue length as the
|
||||||
|
// default packet limit
|
||||||
|
TxQLen: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := netlink.LinkAdd(br)
|
||||||
|
if err != nil && err != syscall.EEXIST {
|
||||||
|
return nil, fmt.Errorf("could not add %q: %v", brName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch link to read all attributes and if it already existed,
|
||||||
|
// ensure it's really a bridge with similar configuration
|
||||||
|
br, err = bridgeByName(brName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.LinkSetUp(br); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return br, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
|
||||||
|
contIface := ¤t.Interface{}
|
||||||
|
hostIface := ¤t.Interface{}
|
||||||
|
|
||||||
|
err := netns.Do(func(hostNS ns.NetNS) error {
|
||||||
|
// create the veth pair in the container and move host end into host netns
|
||||||
|
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
contIface.Name = containerVeth.Name
|
||||||
|
contIface.Mac = containerVeth.HardwareAddr.String()
|
||||||
|
contIface.Sandbox = netns.Path()
|
||||||
|
hostIface.Name = hostVeth.Name
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to lookup hostVeth again as its index has changed during ns move
|
||||||
|
hostVeth, err := netlink.LinkByName(hostIface.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
|
||||||
|
}
|
||||||
|
hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
|
||||||
|
|
||||||
|
// connect host veth end to the bridge
|
||||||
|
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set hairpin mode
|
||||||
|
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostIface, contIface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcGatewayIP(ipn *net.IPNet) net.IP {
|
||||||
|
nid := ipn.IP.Mask(ipn.Mask)
|
||||||
|
return ip.NextIP(nid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
|
||||||
|
// create bridge if necessary
|
||||||
|
br, err := ensureBridge(n.BrName, n.MTU)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return br, ¤t.Interface{
|
||||||
|
Name: br.Attrs().Name,
|
||||||
|
Mac: br.Attrs().HardwareAddr.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
n, cniVersion, err := loadNetConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.IsDefaultGW {
|
||||||
|
n.IsGW = true
|
||||||
|
}
|
||||||
|
|
||||||
|
br, brInterface, err := setupBridge(n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
netns, err := ns.GetNS(args.Netns)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||||
|
}
|
||||||
|
defer netns.Close()
|
||||||
|
|
||||||
|
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the IPAM plugin and get back the config to apply
|
||||||
|
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert whatever the IPAM result was into the current Result type
|
||||||
|
result, err := current.NewResultFromResult(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.IPs) == 0 {
|
||||||
|
return errors.New("IPAM plugin returned missing IP config")
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
|
||||||
|
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
// All IPs currently refer to the container interface
|
||||||
|
ipc.Interface = 2
|
||||||
|
if ipc.Gateway == nil && n.IsGW {
|
||||||
|
ipc.Gateway = calcGatewayIP(&ipc.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netns.Do(func(_ ns.NetNS) error {
|
||||||
|
// set the default gateway if requested
|
||||||
|
if n.IsDefaultGW {
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
defaultNet := &net.IPNet{}
|
||||||
|
switch {
|
||||||
|
case ipc.Address.IP.To4() != nil:
|
||||||
|
defaultNet.IP = net.IPv4zero
|
||||||
|
defaultNet.Mask = net.IPMask(net.IPv4zero)
|
||||||
|
case len(ipc.Address.IP) == net.IPv6len && ipc.Address.IP.To4() == nil:
|
||||||
|
defaultNet.IP = net.IPv6zero
|
||||||
|
defaultNet.Mask = net.IPMask(net.IPv6zero)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown IP object: %v", ipc)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range result.Routes {
|
||||||
|
if defaultNet.String() == route.Dst.String() {
|
||||||
|
if route.GW != nil && !route.GW.Equal(ipc.Gateway) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"isDefaultGateway ineffective because IPAM sets default route via %q",
|
||||||
|
route.GW,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Routes = append(
|
||||||
|
result.Routes,
|
||||||
|
&types.Route{Dst: *defaultNet, GW: ipc.Gateway},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch the veth since its MAC address may changed
|
||||||
|
link, err := netlink.LinkByName(args.IfName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not lookup %q: %v", args.IfName, err)
|
||||||
|
}
|
||||||
|
containerInterface.Mac = link.Attrs().HardwareAddr.String()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.IsGW {
|
||||||
|
var firstV4Addr net.IP
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
gwn := &net.IPNet{
|
||||||
|
IP: ipc.Gateway,
|
||||||
|
Mask: ipc.Address.Mask,
|
||||||
|
}
|
||||||
|
if ipc.Gateway.To4() != nil && firstV4Addr == nil {
|
||||||
|
firstV4Addr = ipc.Gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstV4Addr != nil {
|
||||||
|
if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ip.EnableIP4Forward(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable forwarding: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.IPMasq {
|
||||||
|
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||||
|
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refetch the bridge since its MAC address may change when the first
|
||||||
|
// veth is added or after its IP address is set
|
||||||
|
br, err = bridgeByName(n.BrName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
brInterface.Mac = br.Attrs().HardwareAddr.String()
|
||||||
|
|
||||||
|
result.DNS = n.DNS
|
||||||
|
|
||||||
|
return types.PrintResult(result, cniVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
n, _, err := loadNetConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Netns == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a netns so try to clean up. Delete can be called multiple times
|
||||||
|
// so don't return an error if the device is already removed.
|
||||||
|
// If the device isn't there then don't try to clean up IP masq either.
|
||||||
|
var ipn *net.IPNet
|
||||||
|
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
var err error
|
||||||
|
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
|
||||||
|
if err != nil && err == ip.ErrLinkNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipn != nil && n.IPMasq {
|
||||||
|
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
||||||
|
comment := utils.FormatComment(n.Name, args.ContainerID)
|
||||||
|
err = ip.TeardownIPMasq(ipn, chain, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
27
plugins/main/bridge/bridge_suite_test.go
Normal file
27
plugins/main/bridge/bridge_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
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBridge(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "bridge Suite")
|
||||||
|
}
|
529
plugins/main/bridge/bridge_test.go
Normal file
529
plugins/main/bridge/bridge_test.go
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"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/containernetworking/cni/pkg/utils/hwaddr"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkBridgeConfig03x(version string, originalNS ns.NetNS) {
|
||||||
|
const BRNAME = "cni0"
|
||||||
|
const IFNAME = "eth0"
|
||||||
|
|
||||||
|
gwaddr, subnet, err := net.ParseCIDR("10.1.2.1/24")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "%s",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "bridge",
|
||||||
|
"bridge": "%s",
|
||||||
|
"isDefaultGateway": true,
|
||||||
|
"ipMasq": false,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "%s"
|
||||||
|
}
|
||||||
|
}`, version, BRNAME, subnet.String())
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *current.Result
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(strings.Index(string(raw), "\"interfaces\":")).Should(BeNumerically(">", 0))
|
||||||
|
|
||||||
|
result, err = current.GetResult(r)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(len(result.Interfaces)).To(Equal(3))
|
||||||
|
Expect(result.Interfaces[0].Name).To(Equal(BRNAME))
|
||||||
|
Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
|
||||||
|
|
||||||
|
// Make sure bridge link exists
|
||||||
|
link, err := netlink.LinkByName(result.Interfaces[0].Name)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(BRNAME))
|
||||||
|
Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{}))
|
||||||
|
Expect(link.Attrs().HardwareAddr.String()).To(Equal(result.Interfaces[0].Mac))
|
||||||
|
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
|
||||||
|
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
|
||||||
|
|
||||||
|
// Ensure bridge has gateway address
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(BeNumerically(">", 0))
|
||||||
|
found := false
|
||||||
|
subnetPrefix, subnetBits := subnet.Mask.Size()
|
||||||
|
for _, a := range addrs {
|
||||||
|
aPrefix, aBits := a.IPNet.Mask.Size()
|
||||||
|
if a.IPNet.IP.Equal(gwaddr) && aPrefix == subnetPrefix && aBits == subnetBits {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(found).To(Equal(true))
|
||||||
|
|
||||||
|
// Check for the veth link in the main namespace
|
||||||
|
links, err := netlink.LinkList()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback
|
||||||
|
|
||||||
|
link, err = netlink.LinkByName(result.Interfaces[1].Name)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Find the veth peer in the container namespace and the default route
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
|
||||||
|
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
|
||||||
|
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
|
||||||
|
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
|
||||||
|
|
||||||
|
// Ensure the default route
|
||||||
|
routes, err := netlink.RouteList(link, 0)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var defaultRouteFound bool
|
||||||
|
for _, route := range routes {
|
||||||
|
defaultRouteFound = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwaddr))
|
||||||
|
if defaultRouteFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(defaultRouteFound).To(Equal(true))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure the host veth has been deleted
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure the container veth has been deleted
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(result.Interfaces[1].Name)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("bridge Operations", func() {
|
||||||
|
var originalNS ns.NetNS
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Create a new NetNS so we don't modify the host
|
||||||
|
var err error
|
||||||
|
originalNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("creates a bridge", func() {
|
||||||
|
const IFNAME = "bridge0"
|
||||||
|
|
||||||
|
conf := &NetConf{
|
||||||
|
NetConf: types.NetConf{
|
||||||
|
CNIVersion: "0.3.1",
|
||||||
|
Name: "testConfig",
|
||||||
|
Type: "bridge",
|
||||||
|
},
|
||||||
|
BrName: IFNAME,
|
||||||
|
IsGW: false,
|
||||||
|
IPMasq: false,
|
||||||
|
MTU: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
bridge, _, err := setupBridge(conf)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(bridge.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
|
||||||
|
// Double check that the link was added
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles an existing bridge", func() {
|
||||||
|
const IFNAME = "bridge0"
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := netlink.LinkAdd(&netlink.Bridge{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: IFNAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
ifindex := link.Attrs().Index
|
||||||
|
|
||||||
|
conf := &NetConf{
|
||||||
|
NetConf: types.NetConf{
|
||||||
|
CNIVersion: "0.3.1",
|
||||||
|
Name: "testConfig",
|
||||||
|
Type: "bridge",
|
||||||
|
},
|
||||||
|
BrName: IFNAME,
|
||||||
|
IsGW: false,
|
||||||
|
IPMasq: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge, _, err := setupBridge(conf)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(bridge.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
Expect(bridge.Attrs().Index).To(Equal(ifindex))
|
||||||
|
|
||||||
|
// Double check that the link has the same ifindex
|
||||||
|
link, err = netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
Expect(link.Attrs().Index).To(Equal(ifindex))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.3.0 config", func() {
|
||||||
|
checkBridgeConfig03x("0.3.0", originalNS)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.3.1 config", func() {
|
||||||
|
checkBridgeConfig03x("0.3.1", originalNS)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("deconfigures an unconfigured bridge with DEL", func() {
|
||||||
|
const BRNAME = "cni0"
|
||||||
|
const IFNAME = "eth0"
|
||||||
|
|
||||||
|
_, subnet, err := net.ParseCIDR("10.1.2.1/24")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.3.0",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "bridge",
|
||||||
|
"bridge": "%s",
|
||||||
|
"isDefaultGateway": true,
|
||||||
|
"ipMasq": false,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "%s"
|
||||||
|
}
|
||||||
|
}`, BRNAME, subnet.String())
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() {
|
||||||
|
const BRNAME = "cni0"
|
||||||
|
const IFNAME = "eth0"
|
||||||
|
|
||||||
|
gwaddr, subnet, err := net.ParseCIDR("10.1.2.1/24")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.1.0",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "bridge",
|
||||||
|
"bridge": "%s",
|
||||||
|
"isDefaultGateway": true,
|
||||||
|
"ipMasq": false,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "%s"
|
||||||
|
}
|
||||||
|
}`, BRNAME, subnet.String())
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *types020.Result
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
|
||||||
|
|
||||||
|
// We expect a version 0.1.0 result
|
||||||
|
result, err = types020.GetResult(r)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure bridge link exists
|
||||||
|
link, err := netlink.LinkByName(BRNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(BRNAME))
|
||||||
|
Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{}))
|
||||||
|
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
|
||||||
|
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
|
||||||
|
|
||||||
|
// Ensure bridge has gateway address
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(BeNumerically(">", 0))
|
||||||
|
found := false
|
||||||
|
subnetPrefix, subnetBits := subnet.Mask.Size()
|
||||||
|
for _, a := range addrs {
|
||||||
|
aPrefix, aBits := a.IPNet.Mask.Size()
|
||||||
|
if a.IPNet.IP.Equal(gwaddr) && aPrefix == subnetPrefix && aBits == subnetBits {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(found).To(Equal(true))
|
||||||
|
|
||||||
|
// Check for the veth link in the main namespace; can't
|
||||||
|
// check the for the specific link since version 0.1.0
|
||||||
|
// doesn't report interfaces
|
||||||
|
links, err := netlink.LinkList()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Find the veth peer in the container namespace and the default route
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
|
||||||
|
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
|
||||||
|
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
|
||||||
|
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
|
||||||
|
|
||||||
|
// Ensure the default route
|
||||||
|
routes, err := netlink.RouteList(link, 0)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var defaultRouteFound bool
|
||||||
|
for _, route := range routes {
|
||||||
|
defaultRouteFound = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwaddr))
|
||||||
|
if defaultRouteFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expect(defaultRouteFound).To(Equal(true))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure the container veth has been deleted; cannot check
|
||||||
|
// host veth as version 0.1.0 can't report its name
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("ensure bridge address", func() {
|
||||||
|
const IFNAME = "bridge0"
|
||||||
|
const EXPECTED_IP = "10.0.0.0/8"
|
||||||
|
const CHANGED_EXPECTED_IP = "10.1.2.3/16"
|
||||||
|
|
||||||
|
conf := &NetConf{
|
||||||
|
NetConf: types.NetConf{
|
||||||
|
CNIVersion: "0.3.1",
|
||||||
|
Name: "testConfig",
|
||||||
|
Type: "bridge",
|
||||||
|
},
|
||||||
|
BrName: IFNAME,
|
||||||
|
IsGW: true,
|
||||||
|
IPMasq: false,
|
||||||
|
MTU: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
gwnFirst := &net.IPNet{
|
||||||
|
IP: net.IPv4(10, 0, 0, 0),
|
||||||
|
Mask: net.CIDRMask(8, 32),
|
||||||
|
}
|
||||||
|
|
||||||
|
gwnSecond := &net.IPNet{
|
||||||
|
IP: net.IPv4(10, 1, 2, 3),
|
||||||
|
Mask: net.CIDRMask(16, 32),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
bridge, _, err := setupBridge(conf)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
// Check if ForceAddress has default value
|
||||||
|
Expect(conf.ForceAddress).To(Equal(false))
|
||||||
|
|
||||||
|
err = ensureBridgeAddr(bridge, gwnFirst, conf.ForceAddress)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
//Check if IP address is set correctly
|
||||||
|
addrs, err := netlink.AddrList(bridge, syscall.AF_INET)
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
addr := addrs[0].IPNet.String()
|
||||||
|
Expect(addr).To(Equal(EXPECTED_IP))
|
||||||
|
|
||||||
|
//The bridge IP address has been changed. Error expected when ForceAddress is set to false.
|
||||||
|
err = ensureBridgeAddr(bridge, gwnSecond, false)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
|
||||||
|
//The IP address should stay the same.
|
||||||
|
addrs, err = netlink.AddrList(bridge, syscall.AF_INET)
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
addr = addrs[0].IPNet.String()
|
||||||
|
Expect(addr).To(Equal(EXPECTED_IP))
|
||||||
|
|
||||||
|
//Reconfigure IP when ForceAddress is set to true and IP address has been changed.
|
||||||
|
err = ensureBridgeAddr(bridge, gwnSecond, true)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
//Retrieve the IP address after reconfiguration
|
||||||
|
addrs, err = netlink.AddrList(bridge, syscall.AF_INET)
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
addr = addrs[0].IPNet.String()
|
||||||
|
Expect(addr).To(Equal(CHANGED_EXPECTED_IP))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
210
plugins/main/ipvlan/ipvlan.go
Normal file
210
plugins/main/ipvlan/ipvlan.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
// 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"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ip"
|
||||||
|
"github.com/containernetworking/cni/pkg/ipam"
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"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"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
Master string `json:"master"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// this ensures that main runs only on main thread (thread group leader).
|
||||||
|
// since namespace ops (unshare, setns) are done for a single thread, we
|
||||||
|
// must ensure that the goroutine does not jump from OS thread to thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConf(bytes []byte) (*NetConf, string, error) {
|
||||||
|
n := &NetConf{}
|
||||||
|
if err := json.Unmarshal(bytes, n); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
if n.Master == "" {
|
||||||
|
return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||||
|
}
|
||||||
|
return n, n.CNIVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modeFromString(s string) (netlink.IPVlanMode, error) {
|
||||||
|
switch s {
|
||||||
|
case "", "l2":
|
||||||
|
return netlink.IPVLAN_MODE_L2, nil
|
||||||
|
case "l3":
|
||||||
|
return netlink.IPVLAN_MODE_L3, nil
|
||||||
|
case "l3s":
|
||||||
|
return netlink.IPVLAN_MODE_L3S, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown ipvlan mode: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||||
|
ipvlan := ¤t.Interface{}
|
||||||
|
|
||||||
|
mode, err := modeFromString(conf.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := netlink.LinkByName(conf.Master)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// due to kernel bug we have to create with tmpname or it might
|
||||||
|
// collide with the name on the host and error out
|
||||||
|
tmpName, err := ip.RandomVethName()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mv := &netlink.IPVlan{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
MTU: conf.MTU,
|
||||||
|
Name: tmpName,
|
||||||
|
ParentIndex: m.Attrs().Index,
|
||||||
|
Namespace: netlink.NsFd(int(netns.Fd())),
|
||||||
|
},
|
||||||
|
Mode: mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.LinkAdd(mv); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ipvlan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netns.Do(func(_ ns.NetNS) error {
|
||||||
|
err := ip.RenameLink(tmpName, ifName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err)
|
||||||
|
}
|
||||||
|
ipvlan.Name = ifName
|
||||||
|
|
||||||
|
// Re-fetch ipvlan to get all properties/attributes
|
||||||
|
contIpvlan, err := netlink.LinkByName(ipvlan.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to refetch ipvlan %q: %v", ipvlan.Name, err)
|
||||||
|
}
|
||||||
|
ipvlan.Mac = contIpvlan.Attrs().HardwareAddr.String()
|
||||||
|
ipvlan.Sandbox = netns.Path()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipvlan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
n, cniVersion, err := loadConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
netns, err := ns.GetNS(args.Netns)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||||
|
}
|
||||||
|
defer netns.Close()
|
||||||
|
|
||||||
|
ipvlanInterface, err := createIpvlan(n, args.IfName, netns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the IPAM plugin and get back the config to apply
|
||||||
|
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Convert whatever the IPAM result was into the current Result type
|
||||||
|
result, err := current.NewResultFromResult(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.IPs) == 0 {
|
||||||
|
return errors.New("IPAM plugin returned missing IP config")
|
||||||
|
}
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
// All addresses belong to the ipvlan interface
|
||||||
|
ipc.Interface = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Interfaces = []*current.Interface{ipvlanInterface}
|
||||||
|
|
||||||
|
err = netns.Do(func(_ ns.NetNS) error {
|
||||||
|
return ipam.ConfigureIface(args.IfName, result)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.DNS = n.DNS
|
||||||
|
|
||||||
|
return types.PrintResult(result, cniVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
n, _, err := loadConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Netns == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a netns so try to clean up. Delete can be called multiple times
|
||||||
|
// so don't return an error if the device is already removed.
|
||||||
|
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
if _, err := ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4); err != nil {
|
||||||
|
if err != ip.ErrLinkNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
27
plugins/main/ipvlan/ipvlan_suite_test.go
Normal file
27
plugins/main/ipvlan/ipvlan_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
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIpvlan(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "ipvlan Suite")
|
||||||
|
}
|
227
plugins/main/ipvlan/ipvlan_test.go
Normal file
227
plugins/main/ipvlan/ipvlan_test.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
// 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"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/testutils"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MASTER_NAME = "eth0"
|
||||||
|
|
||||||
|
var _ = Describe("ipvlan Operations", func() {
|
||||||
|
var originalNS ns.NetNS
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Create a new NetNS so we don't modify the host
|
||||||
|
var err error
|
||||||
|
originalNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
// Add master
|
||||||
|
err = netlink.LinkAdd(&netlink.Dummy{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: MASTER_NAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, err = netlink.LinkByName(MASTER_NAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("creates an ipvlan link in a non-default namespace", func() {
|
||||||
|
conf := &NetConf{
|
||||||
|
NetConf: types.NetConf{
|
||||||
|
CNIVersion: "0.3.1",
|
||||||
|
Name: "testConfig",
|
||||||
|
Type: "ipvlan",
|
||||||
|
},
|
||||||
|
Master: MASTER_NAME,
|
||||||
|
Mode: "l2",
|
||||||
|
MTU: 1500,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ipvlan in other namespace
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, err := createIpvlan(conf, "foobar0", targetNs)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure ipvlan link exists in the target namespace
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName("foobar0")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal("foobar0"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures an iplvan link with ADD/DEL", func() {
|
||||||
|
const IFNAME = "ipvl0"
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "ipvlan",
|
||||||
|
"master": "%s",
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`, MASTER_NAME)
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *current.Result
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
result, err = current.GetResult(r)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(len(result.Interfaces)).To(Equal(1))
|
||||||
|
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||||
|
Expect(len(result.IPs)).To(Equal(1))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure ipvlan link exists in the target namespace
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
|
||||||
|
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||||
|
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure ipvlan link has been deleted
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("deconfigures an unconfigured ipvlan link with DEL", func() {
|
||||||
|
const IFNAME = "ipvl0"
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.3.0",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "ipvlan",
|
||||||
|
"master": "%s",
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`, MASTER_NAME)
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
72
plugins/main/loopback/loopback.go
Normal file
72
plugins/main/loopback/loopback.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// 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/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
args.IfName = "lo" // ignore config, this only works for loopback
|
||||||
|
err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
link, err := netlink.LinkByName(args.IfName)
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.LinkSetUp(link)
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
result := current.Result{}
|
||||||
|
return result.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
args.IfName = "lo" // ignore config, this only works for loopback
|
||||||
|
err := ns.WithNetNSPath(args.Netns, func(ns.NetNS) error {
|
||||||
|
link, err := netlink.LinkByName(args.IfName)
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.LinkSetDown(link)
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err // not tested
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
41
plugins/main/loopback/loopback_suite_test.go
Normal file
41
plugins/main/loopback/loopback_suite_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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/gomega/gexec"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pathToLoPlugin string
|
||||||
|
|
||||||
|
func TestLoopback(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Loopback Suite")
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = BeforeSuite(func() {
|
||||||
|
var err error
|
||||||
|
pathToLoPlugin, err = gexec.Build("github.com/containernetworking/plugins/plugins/main/loopback")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = AfterSuite(func() {
|
||||||
|
gexec.CleanupBuildArtifacts()
|
||||||
|
})
|
100
plugins/main/loopback/loopback_test.go
Normal file
100
plugins/main/loopback/loopback_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/onsi/gomega/gbytes"
|
||||||
|
"github.com/onsi/gomega/gexec"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Loopback", func() {
|
||||||
|
var (
|
||||||
|
networkNS ns.NetNS
|
||||||
|
containerID string
|
||||||
|
command *exec.Cmd
|
||||||
|
environ []string
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
command = exec.Command(pathToLoPlugin)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
networkNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
environ = []string{
|
||||||
|
fmt.Sprintf("CNI_CONTAINERID=%s", containerID),
|
||||||
|
fmt.Sprintf("CNI_NETNS=%s", networkNS.Path()),
|
||||||
|
fmt.Sprintf("CNI_IFNAME=%s", "this is ignored"),
|
||||||
|
fmt.Sprintf("CNI_ARGS=%s", "none"),
|
||||||
|
fmt.Sprintf("CNI_PATH=%s", "/some/test/path"),
|
||||||
|
}
|
||||||
|
command.Stdin = strings.NewReader(`{ "cniVersion": "0.1.0" }`)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(networkNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when given a network namespace", func() {
|
||||||
|
It("sets the lo device to UP", func() {
|
||||||
|
command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "ADD"))
|
||||||
|
|
||||||
|
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Eventually(session).Should(gbytes.Say(`{.*}`))
|
||||||
|
Eventually(session).Should(gexec.Exit(0))
|
||||||
|
|
||||||
|
var lo *net.Interface
|
||||||
|
err = networkNS.Do(func(ns.NetNS) error {
|
||||||
|
var err error
|
||||||
|
lo, err = net.InterfaceByName("lo")
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(lo.Flags & net.FlagUp).To(Equal(net.FlagUp))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("sets the lo device to DOWN", func() {
|
||||||
|
command.Env = append(environ, fmt.Sprintf("CNI_COMMAND=%s", "DEL"))
|
||||||
|
|
||||||
|
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Eventually(session).Should(gbytes.Say(``))
|
||||||
|
Eventually(session).Should(gexec.Exit(0))
|
||||||
|
|
||||||
|
var lo *net.Interface
|
||||||
|
err = networkNS.Do(func(ns.NetNS) error {
|
||||||
|
var err error
|
||||||
|
lo, err = net.InterfaceByName("lo")
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(lo.Flags & net.FlagUp).NotTo(Equal(net.FlagUp))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
251
plugins/main/macvlan/macvlan.go
Normal file
251
plugins/main/macvlan/macvlan.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
// 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"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ip"
|
||||||
|
"github.com/containernetworking/cni/pkg/ipam"
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/utils/sysctl"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IPv4InterfaceArpProxySysctlTemplate = "net.ipv4.conf.%s.proxy_arp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
Master string `json:"master"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// this ensures that main runs only on main thread (thread group leader).
|
||||||
|
// since namespace ops (unshare, setns) are done for a single thread, we
|
||||||
|
// must ensure that the goroutine does not jump from OS thread to thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConf(bytes []byte) (*NetConf, string, error) {
|
||||||
|
n := &NetConf{}
|
||||||
|
if err := json.Unmarshal(bytes, n); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
if n.Master == "" {
|
||||||
|
return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
|
||||||
|
}
|
||||||
|
return n, n.CNIVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modeFromString(s string) (netlink.MacvlanMode, error) {
|
||||||
|
switch s {
|
||||||
|
case "", "bridge":
|
||||||
|
return netlink.MACVLAN_MODE_BRIDGE, nil
|
||||||
|
case "private":
|
||||||
|
return netlink.MACVLAN_MODE_PRIVATE, nil
|
||||||
|
case "vepa":
|
||||||
|
return netlink.MACVLAN_MODE_VEPA, nil
|
||||||
|
case "passthru":
|
||||||
|
return netlink.MACVLAN_MODE_PASSTHRU, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown macvlan mode: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
|
||||||
|
macvlan := ¤t.Interface{}
|
||||||
|
|
||||||
|
mode, err := modeFromString(conf.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := netlink.LinkByName(conf.Master)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// due to kernel bug we have to create with tmpName or it might
|
||||||
|
// collide with the name on the host and error out
|
||||||
|
tmpName, err := ip.RandomVethName()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mv := &netlink.Macvlan{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
MTU: conf.MTU,
|
||||||
|
Name: tmpName,
|
||||||
|
ParentIndex: m.Attrs().Index,
|
||||||
|
Namespace: netlink.NsFd(int(netns.Fd())),
|
||||||
|
},
|
||||||
|
Mode: mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.LinkAdd(mv); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create macvlan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netns.Do(func(_ ns.NetNS) error {
|
||||||
|
// TODO: duplicate following lines for ipv6 support, when it will be added in other places
|
||||||
|
ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
|
||||||
|
if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
|
||||||
|
// remove the newly added link and ignore errors, because we already are in a failed state
|
||||||
|
_ = netlink.LinkDel(mv)
|
||||||
|
return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ip.RenameLink(tmpName, ifName)
|
||||||
|
if err != nil {
|
||||||
|
_ = netlink.LinkDel(mv)
|
||||||
|
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
|
||||||
|
}
|
||||||
|
macvlan.Name = ifName
|
||||||
|
|
||||||
|
// Re-fetch macvlan to get all properties/attributes
|
||||||
|
contMacvlan, err := netlink.LinkByName(ifName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err)
|
||||||
|
}
|
||||||
|
macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String()
|
||||||
|
macvlan.Sandbox = netns.Path()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return macvlan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
n, cniVersion, err := loadConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
netns, err := ns.GetNS(args.Netns)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open netns %q: %v", netns, err)
|
||||||
|
}
|
||||||
|
defer netns.Close()
|
||||||
|
|
||||||
|
macvlanInterface, err := createMacvlan(n, args.IfName, netns)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the IPAM plugin and get back the config to apply
|
||||||
|
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Convert whatever the IPAM result was into the current Result type
|
||||||
|
result, err := current.NewResultFromResult(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.IPs) == 0 {
|
||||||
|
return errors.New("IPAM plugin returned missing IP config")
|
||||||
|
}
|
||||||
|
result.Interfaces = []*current.Interface{macvlanInterface}
|
||||||
|
|
||||||
|
var firstV4Addr net.IP
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
// All addresses apply to the container macvlan interface
|
||||||
|
ipc.Interface = 0
|
||||||
|
|
||||||
|
if ipc.Address.IP.To4() != nil && firstV4Addr == nil {
|
||||||
|
firstV4Addr = ipc.Address.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstV4Addr != nil {
|
||||||
|
err = netns.Do(func(_ ns.NetNS) error {
|
||||||
|
if err := ip.SetHWAddrByIP(args.IfName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipam.ConfigureIface(args.IfName, result)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch macvlan interface as its MAC address may have changed
|
||||||
|
err = netns.Do(func(_ ns.NetNS) error {
|
||||||
|
link, err := netlink.LinkByName(args.IfName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to re-fetch macvlan interface: %v", err)
|
||||||
|
}
|
||||||
|
macvlanInterface.Mac = link.Attrs().HardwareAddr.String()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.DNS = n.DNS
|
||||||
|
|
||||||
|
return types.PrintResult(result, cniVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
n, _, err := loadConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Netns == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a netns so try to clean up. Delete can be called multiple times
|
||||||
|
// so don't return an error if the device is already removed.
|
||||||
|
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
if _, err := ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4); err != nil {
|
||||||
|
if err != ip.ErrLinkNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
27
plugins/main/macvlan/macvlan_suite_test.go
Normal file
27
plugins/main/macvlan/macvlan_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
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMacvlan(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "macvlan Suite")
|
||||||
|
}
|
230
plugins/main/macvlan/macvlan_test.go
Normal file
230
plugins/main/macvlan/macvlan_test.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
// 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"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/testutils"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/utils/hwaddr"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MASTER_NAME = "eth0"
|
||||||
|
|
||||||
|
var _ = Describe("macvlan Operations", func() {
|
||||||
|
var originalNS ns.NetNS
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Create a new NetNS so we don't modify the host
|
||||||
|
var err error
|
||||||
|
originalNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
// Add master
|
||||||
|
err = netlink.LinkAdd(&netlink.Dummy{
|
||||||
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
|
Name: MASTER_NAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, err = netlink.LinkByName(MASTER_NAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("creates an macvlan link in a non-default namespace", func() {
|
||||||
|
conf := &NetConf{
|
||||||
|
NetConf: types.NetConf{
|
||||||
|
CNIVersion: "0.3.1",
|
||||||
|
Name: "testConfig",
|
||||||
|
Type: "macvlan",
|
||||||
|
},
|
||||||
|
Master: MASTER_NAME,
|
||||||
|
Mode: "bridge",
|
||||||
|
MTU: 1500,
|
||||||
|
}
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, err = createMacvlan(conf, "foobar0", targetNs)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure macvlan link exists in the target namespace
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName("foobar0")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal("foobar0"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures a macvlan link with ADD/DEL", func() {
|
||||||
|
const IFNAME = "macvl0"
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "macvlan",
|
||||||
|
"master": "%s",
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`, MASTER_NAME)
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *current.Result
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
result, err = current.GetResult(r)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(len(result.Interfaces)).To(Equal(1))
|
||||||
|
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
|
||||||
|
Expect(len(result.IPs)).To(Equal(1))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure macvlan link exists in the target namespace
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
|
||||||
|
hwaddrString := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
|
||||||
|
Expect(hwaddrString).To(HavePrefix(hwaddr.PrivateMACPrefixString))
|
||||||
|
|
||||||
|
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
|
||||||
|
|
||||||
|
addrs, err := netlink.AddrList(link, syscall.AF_INET)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(len(addrs)).To(Equal(1))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure macvlan link has been deleted
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("deconfigures an unconfigured macvlan link with DEL", func() {
|
||||||
|
const IFNAME = "macvl0"
|
||||||
|
|
||||||
|
conf := fmt.Sprintf(`{
|
||||||
|
"cniVersion": "0.3.0",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "macvlan",
|
||||||
|
"master": "%s",
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`, MASTER_NAME)
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
299
plugins/main/ptp/ptp.go
Normal file
299
plugins/main/ptp/ptp.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
// 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"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ip"
|
||||||
|
"github.com/containernetworking/cni/pkg/ipam"
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/types/current"
|
||||||
|
"github.com/containernetworking/cni/pkg/utils"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// this ensures that main runs only on main thread (thread group leader).
|
||||||
|
// since namespace ops (unshare, setns) are done for a single thread, we
|
||||||
|
// must ensure that the goroutine does not jump from OS thread to thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
IPMasq bool `json:"ipMasq"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
|
||||||
|
// The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1.
|
||||||
|
// What we want is really a point-to-point link but veth does not support IFF_POINTOPONT.
|
||||||
|
// Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
|
||||||
|
// add a route like "192.168.3.0/24 via 192.168.3.1 dev $ifName".
|
||||||
|
// Unfortunately that won't work as the GW will be outside the interface's subnet.
|
||||||
|
|
||||||
|
// Our solution is to configure the interface with 192.168.3.5/24, then delete the
|
||||||
|
// "192.168.3.0/24 dev $ifName" route that was automatically added. Then we add
|
||||||
|
// "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName".
|
||||||
|
// In other words we force all traffic to ARP via the gateway except for GW itself.
|
||||||
|
|
||||||
|
hostInterface := ¤t.Interface{}
|
||||||
|
containerInterface := ¤t.Interface{}
|
||||||
|
|
||||||
|
err := netns.Do(func(hostNS ns.NetNS) error {
|
||||||
|
hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hostInterface.Name = hostVeth.Name
|
||||||
|
hostInterface.Mac = hostVeth.HardwareAddr.String()
|
||||||
|
containerInterface.Name = contVeth0.Name
|
||||||
|
containerInterface.Mac = contVeth0.HardwareAddr.String()
|
||||||
|
containerInterface.Sandbox = netns.Path()
|
||||||
|
|
||||||
|
var firstV4Addr net.IP
|
||||||
|
for _, ipc := range pr.IPs {
|
||||||
|
// All addresses apply to the container veth interface
|
||||||
|
ipc.Interface = 1
|
||||||
|
|
||||||
|
if ipc.Address.IP.To4() != nil && firstV4Addr == nil {
|
||||||
|
firstV4Addr = ipc.Address.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.Interfaces = []*current.Interface{hostInterface, containerInterface}
|
||||||
|
|
||||||
|
if firstV4Addr != nil {
|
||||||
|
err = hostNS.Do(func(_ ns.NetNS) error {
|
||||||
|
hostVethName := hostVeth.Name
|
||||||
|
if err := ip.SetHWAddrByIP(hostVethName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
|
||||||
|
return fmt.Errorf("failed to set hardware addr by IP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ipam.ConfigureIface(ifName, pr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ip.SetHWAddrByIP(contVeth0.Name, firstV4Addr, nil /* TODO IPv6 */); err != nil {
|
||||||
|
return fmt.Errorf("failed to set hardware addr by IP: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-fetch container veth to update attributes
|
||||||
|
contVeth, err := netlink.LinkByName(ifName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to look up %q: %v", ifName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipc := range pr.IPs {
|
||||||
|
// Delete the route that was automatically added
|
||||||
|
route := netlink.Route{
|
||||||
|
LinkIndex: contVeth.Attrs().Index,
|
||||||
|
Dst: &net.IPNet{
|
||||||
|
IP: ipc.Address.IP.Mask(ipc.Address.Mask),
|
||||||
|
Mask: ipc.Address.Mask,
|
||||||
|
},
|
||||||
|
Scope: netlink.SCOPE_NOWHERE,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := netlink.RouteDel(&route); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete route %v: %v", route, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range []netlink.Route{
|
||||||
|
netlink.Route{
|
||||||
|
LinkIndex: contVeth.Attrs().Index,
|
||||||
|
Dst: &net.IPNet{
|
||||||
|
IP: ipc.Gateway,
|
||||||
|
Mask: net.CIDRMask(32, 32),
|
||||||
|
},
|
||||||
|
Scope: netlink.SCOPE_LINK,
|
||||||
|
Src: ipc.Address.IP,
|
||||||
|
},
|
||||||
|
netlink.Route{
|
||||||
|
LinkIndex: contVeth.Attrs().Index,
|
||||||
|
Dst: &net.IPNet{
|
||||||
|
IP: ipc.Address.IP.Mask(ipc.Address.Mask),
|
||||||
|
Mask: ipc.Address.Mask,
|
||||||
|
},
|
||||||
|
Scope: netlink.SCOPE_UNIVERSE,
|
||||||
|
Gw: ipc.Gateway,
|
||||||
|
Src: ipc.Address.IP,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if err := netlink.RouteAdd(&r); err != nil {
|
||||||
|
return fmt.Errorf("failed to add route %v: %v", r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return hostInterface, containerInterface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHostVeth(vethName string, result *current.Result) error {
|
||||||
|
// hostVeth moved namespaces and may have a new ifindex
|
||||||
|
veth, err := netlink.LinkByName(vethName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to lookup %q: %v", vethName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
maskLen := 128
|
||||||
|
if ipc.Address.IP.To4() != nil {
|
||||||
|
maskLen = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
ipn := &net.IPNet{
|
||||||
|
IP: ipc.Gateway,
|
||||||
|
Mask: net.CIDRMask(maskLen, maskLen),
|
||||||
|
}
|
||||||
|
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
||||||
|
if err = netlink.AddrAdd(veth, addr); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IP addr (%#v) to veth: %v", ipn, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipn = &net.IPNet{
|
||||||
|
IP: ipc.Address.IP,
|
||||||
|
Mask: net.CIDRMask(maskLen, maskLen),
|
||||||
|
}
|
||||||
|
// dst happens to be the same as IP/net of host veth
|
||||||
|
if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) {
|
||||||
|
return fmt.Errorf("failed to add route on host: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
conf := NetConf{}
|
||||||
|
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||||
|
return fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ip.EnableIP4Forward(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable forwarding: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the IPAM plugin and get back the config to apply
|
||||||
|
r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Convert whatever the IPAM result was into the current Result type
|
||||||
|
result, err := current.NewResultFromResult(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.IPs) == 0 {
|
||||||
|
return errors.New("IPAM plugin returned missing IP config")
|
||||||
|
}
|
||||||
|
|
||||||
|
netns, err := ns.GetNS(args.Netns)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
||||||
|
}
|
||||||
|
defer netns.Close()
|
||||||
|
|
||||||
|
hostInterface, containerInterface, err := setupContainerVeth(netns, args.IfName, conf.MTU, result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = setupHostVeth(hostInterface.Name, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.IPMasq {
|
||||||
|
chain := utils.FormatChainName(conf.Name, args.ContainerID)
|
||||||
|
comment := utils.FormatComment(conf.Name, args.ContainerID)
|
||||||
|
for _, ipc := range result.IPs {
|
||||||
|
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.DNS = conf.DNS
|
||||||
|
result.Interfaces = []*current.Interface{hostInterface, containerInterface}
|
||||||
|
|
||||||
|
return types.PrintResult(result, conf.CNIVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
conf := NetConf{}
|
||||||
|
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||||
|
return fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipam.ExecDel(conf.IPAM.Type, args.StdinData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Netns == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a netns so try to clean up. Delete can be called multiple times
|
||||||
|
// so don't return an error if the device is already removed.
|
||||||
|
// If the device isn't there then don't try to clean up IP masq either.
|
||||||
|
var ipn *net.IPNet
|
||||||
|
err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
var err error
|
||||||
|
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
|
||||||
|
if err != nil && err == ip.ErrLinkNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipn != nil && conf.IPMasq {
|
||||||
|
chain := utils.FormatChainName(conf.Name, args.ContainerID)
|
||||||
|
comment := utils.FormatComment(conf.Name, args.ContainerID)
|
||||||
|
err = ip.TeardownIPMasq(ipn, chain, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
27
plugins/main/ptp/ptp_suite_test.go
Normal file
27
plugins/main/ptp/ptp_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
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPtp(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "ptp Suite")
|
||||||
|
}
|
152
plugins/main/ptp/ptp_test.go
Normal file
152
plugins/main/ptp/ptp_test.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// 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/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/testutils"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("ptp Operations", func() {
|
||||||
|
var originalNS ns.NetNS
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Create a new NetNS so we don't modify the host
|
||||||
|
var err error
|
||||||
|
originalNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("configures and deconfigures a ptp link with ADD/DEL", func() {
|
||||||
|
const IFNAME = "ptp0"
|
||||||
|
|
||||||
|
conf := `{
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "ptp",
|
||||||
|
"ipMasq": true,
|
||||||
|
"mtu": 5000,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the plugin with the ADD command, creating the veth endpoints
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure ptp link exists in the target namespace
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(link.Attrs().Name).To(Equal(IFNAME))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Call the plugins with the DEL command, deleting the veth endpoints
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Make sure ptp link has been deleted
|
||||||
|
err = targetNs.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
link, err := netlink.LinkByName(IFNAME)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(link).To(BeNil())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
It("deconfigures an unconfigured ptp link with DEL", func() {
|
||||||
|
const IFNAME = "ptp0"
|
||||||
|
|
||||||
|
conf := `{
|
||||||
|
"cniVersion": "0.3.0",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "ptp",
|
||||||
|
"ipMasq": true,
|
||||||
|
"mtu": 5000,
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "dummy",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(conf),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the plugins with the DEL command. It should not error even though the veth doesn't exist.
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
266
plugins/meta/flannel/flannel.go
Normal file
266
plugins/meta/flannel/flannel.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This is a "meta-plugin". It reads in its own netconf, combines it with
|
||||||
|
// the data from flannel generated subnet file and then invokes a plugin
|
||||||
|
// like bridge or ipvlan to do the real work.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/invoke"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSubnetFile = "/run/flannel/subnet.env"
|
||||||
|
defaultDataDir = "/var/lib/cni/flannel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
SubnetFile string `json:"subnetFile"`
|
||||||
|
DataDir string `json:"dataDir"`
|
||||||
|
Delegate map[string]interface{} `json:"delegate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type subnetEnv struct {
|
||||||
|
nw *net.IPNet
|
||||||
|
sn *net.IPNet
|
||||||
|
mtu *uint
|
||||||
|
ipmasq *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se *subnetEnv) missing() string {
|
||||||
|
m := []string{}
|
||||||
|
|
||||||
|
if se.nw == nil {
|
||||||
|
m = append(m, "FLANNEL_NETWORK")
|
||||||
|
}
|
||||||
|
if se.sn == nil {
|
||||||
|
m = append(m, "FLANNEL_SUBNET")
|
||||||
|
}
|
||||||
|
if se.mtu == nil {
|
||||||
|
m = append(m, "FLANNEL_MTU")
|
||||||
|
}
|
||||||
|
if se.ipmasq == nil {
|
||||||
|
m = append(m, "FLANNEL_IPMASQ")
|
||||||
|
}
|
||||||
|
return strings.Join(m, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFlannelNetConf(bytes []byte) (*NetConf, error) {
|
||||||
|
n := &NetConf{
|
||||||
|
SubnetFile: defaultSubnetFile,
|
||||||
|
DataDir: defaultDataDir,
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bytes, n); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFlannelSubnetEnv(fn string) (*subnetEnv, error) {
|
||||||
|
f, err := os.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
se := &subnetEnv{}
|
||||||
|
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
for s.Scan() {
|
||||||
|
parts := strings.SplitN(s.Text(), "=", 2)
|
||||||
|
switch parts[0] {
|
||||||
|
case "FLANNEL_NETWORK":
|
||||||
|
_, se.nw, err = net.ParseCIDR(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "FLANNEL_SUBNET":
|
||||||
|
_, se.sn, err = net.ParseCIDR(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "FLANNEL_MTU":
|
||||||
|
mtu, err := strconv.ParseUint(parts[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
se.mtu = new(uint)
|
||||||
|
*se.mtu = uint(mtu)
|
||||||
|
|
||||||
|
case "FLANNEL_IPMASQ":
|
||||||
|
ipmasq := parts[1] == "true"
|
||||||
|
se.ipmasq = &ipmasq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m := se.missing(); m != "" {
|
||||||
|
return nil, fmt.Errorf("%v is missing %v", fn, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return se, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveScratchNetConf(containerID, dataDir string, netconf []byte) error {
|
||||||
|
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := filepath.Join(dataDir, containerID)
|
||||||
|
return ioutil.WriteFile(path, netconf, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeScratchNetConf(containerID, dataDir string) ([]byte, error) {
|
||||||
|
path := filepath.Join(dataDir, containerID)
|
||||||
|
// Ignore errors when removing - Per spec safe to continue during DEL
|
||||||
|
defer os.Remove(path)
|
||||||
|
|
||||||
|
return ioutil.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delegateAdd(cid, dataDir string, netconf map[string]interface{}) error {
|
||||||
|
netconfBytes, err := json.Marshal(netconf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error serializing delegate netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the rendered netconf for cmdDel
|
||||||
|
if err = saveScratchNetConf(cid, dataDir, netconfBytes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := invoke.DelegateAdd(netconf["type"].(string), netconfBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKey(m map[string]interface{}, k string) bool {
|
||||||
|
_, ok := m[k]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func isString(i interface{}) bool {
|
||||||
|
_, ok := i.(string)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
n, err := loadFlannelNetConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fenv, err := loadFlannelSubnetEnv(n.SubnetFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Delegate == nil {
|
||||||
|
n.Delegate = make(map[string]interface{})
|
||||||
|
} else {
|
||||||
|
if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) {
|
||||||
|
return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field")
|
||||||
|
}
|
||||||
|
if hasKey(n.Delegate, "name") {
|
||||||
|
return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel")
|
||||||
|
}
|
||||||
|
if hasKey(n.Delegate, "ipam") {
|
||||||
|
return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Delegate["name"] = n.Name
|
||||||
|
|
||||||
|
if !hasKey(n.Delegate, "type") {
|
||||||
|
n.Delegate["type"] = "bridge"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasKey(n.Delegate, "ipMasq") {
|
||||||
|
// if flannel is not doing ipmasq, we should
|
||||||
|
ipmasq := !*fenv.ipmasq
|
||||||
|
n.Delegate["ipMasq"] = ipmasq
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasKey(n.Delegate, "mtu") {
|
||||||
|
mtu := fenv.mtu
|
||||||
|
n.Delegate["mtu"] = mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Delegate["type"].(string) == "bridge" {
|
||||||
|
if !hasKey(n.Delegate, "isGateway") {
|
||||||
|
n.Delegate["isGateway"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Delegate["ipam"] = map[string]interface{}{
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": fenv.sn.String(),
|
||||||
|
"routes": []types.Route{
|
||||||
|
types.Route{
|
||||||
|
Dst: *fenv.nw,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return delegateAdd(args.ContainerID, n.DataDir, n.Delegate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
nc, err := loadFlannelNetConf(args.StdinData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
netconfBytes, err := consumeScratchNetConf(args.ContainerID, nc.DataDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// Per spec should ignore error if resources are missing / already removed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n := &types.NetConf{}
|
||||||
|
if err = json.Unmarshal(netconfBytes, n); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoke.DelegateDel(n.Type, netconfBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
26
plugins/meta/flannel/flannel_suite_test.go
Normal file
26
plugins/meta/flannel/flannel_suite_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFlannel(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Flannel Suite")
|
||||||
|
}
|
211
plugins/meta/flannel/flannel_test.go
Normal file
211
plugins/meta/flannel/flannel_test.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
// 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"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
|
"github.com/containernetworking/cni/pkg/testutils"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Flannel", func() {
|
||||||
|
var (
|
||||||
|
originalNS ns.NetNS
|
||||||
|
input string
|
||||||
|
subnetFile string
|
||||||
|
dataDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
var err error
|
||||||
|
originalNS, err = ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
Expect(originalNS.Close()).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
const inputTemplate = `
|
||||||
|
{
|
||||||
|
"name": "cni-flannel",
|
||||||
|
"type": "flannel",
|
||||||
|
"subnetFile": "%s",
|
||||||
|
"dataDir": "%s"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const flannelSubnetEnv = `
|
||||||
|
FLANNEL_NETWORK=10.1.0.0/16
|
||||||
|
FLANNEL_SUBNET=10.1.17.1/24
|
||||||
|
FLANNEL_MTU=1472
|
||||||
|
FLANNEL_IPMASQ=true
|
||||||
|
`
|
||||||
|
|
||||||
|
var writeSubnetEnv = func(contents string) string {
|
||||||
|
file, err := ioutil.TempFile("", "subnet.env")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, err = file.WriteString(contents)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
return file.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
var err error
|
||||||
|
// flannel subnet.env
|
||||||
|
subnetFile = writeSubnetEnv(flannelSubnetEnv)
|
||||||
|
|
||||||
|
// flannel state dir
|
||||||
|
dataDir, err = ioutil.TempDir("", "dataDir")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
input = fmt.Sprintf(inputTemplate, subnetFile, dataDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
os.Remove(subnetFile)
|
||||||
|
os.Remove(dataDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("CNI lifecycle", func() {
|
||||||
|
It("uses dataDir for storing network configuration", func() {
|
||||||
|
const IFNAME = "eth0"
|
||||||
|
|
||||||
|
targetNs, err := ns.NewNS()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer targetNs.Close()
|
||||||
|
|
||||||
|
args := &skel.CmdArgs{
|
||||||
|
ContainerID: "some-container-id",
|
||||||
|
Netns: targetNs.Path(),
|
||||||
|
IfName: IFNAME,
|
||||||
|
StdinData: []byte(input),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = originalNS.Do(func(ns.NetNS) error {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
|
||||||
|
By("calling ADD")
|
||||||
|
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(input), func() error {
|
||||||
|
return cmdAdd(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("check that plugin writes to net config to dataDir")
|
||||||
|
path := fmt.Sprintf("%s/%s", dataDir, "some-container-id")
|
||||||
|
Expect(path).Should(BeAnExistingFile())
|
||||||
|
|
||||||
|
netConfBytes, err := ioutil.ReadFile(path)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
expected := `{
|
||||||
|
"ipMasq" : false,
|
||||||
|
"ipam" : {
|
||||||
|
"routes" : [
|
||||||
|
{
|
||||||
|
"dst" : "10.1.0.0/16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subnet" : "10.1.17.0/24",
|
||||||
|
"type" : "host-local"
|
||||||
|
},
|
||||||
|
"isGateway": true,
|
||||||
|
"mtu" : 1472,
|
||||||
|
"name" : "cni-flannel",
|
||||||
|
"type" : "bridge"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
Expect(netConfBytes).Should(MatchJSON(expected))
|
||||||
|
|
||||||
|
By("calling DEL")
|
||||||
|
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("check that plugin removes net config from state dir")
|
||||||
|
Expect(path).ShouldNot(BeAnExistingFile())
|
||||||
|
|
||||||
|
By("calling DEL again")
|
||||||
|
err = testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
|
||||||
|
return cmdDel(args)
|
||||||
|
})
|
||||||
|
By("check that plugin does not fail due to missing net config")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("loadFlannelNetConf", func() {
|
||||||
|
Context("when subnetFile and dataDir are specified", func() {
|
||||||
|
It("loads flannel network config", func() {
|
||||||
|
conf, err := loadFlannelNetConf([]byte(input))
|
||||||
|
Expect(err).ShouldNot(HaveOccurred())
|
||||||
|
Expect(conf.Name).To(Equal("cni-flannel"))
|
||||||
|
Expect(conf.Type).To(Equal("flannel"))
|
||||||
|
Expect(conf.SubnetFile).To(Equal(subnetFile))
|
||||||
|
Expect(conf.DataDir).To(Equal(dataDir))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when defaulting subnetFile and dataDir", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
input = `{
|
||||||
|
"name": "cni-flannel",
|
||||||
|
"type": "flannel"
|
||||||
|
}`
|
||||||
|
})
|
||||||
|
|
||||||
|
It("loads flannel network config with defaults", func() {
|
||||||
|
conf, err := loadFlannelNetConf([]byte(input))
|
||||||
|
Expect(err).ShouldNot(HaveOccurred())
|
||||||
|
Expect(conf.Name).To(Equal("cni-flannel"))
|
||||||
|
Expect(conf.Type).To(Equal("flannel"))
|
||||||
|
Expect(conf.SubnetFile).To(Equal(defaultSubnetFile))
|
||||||
|
Expect(conf.DataDir).To(Equal(defaultDataDir))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("loadFlannelSubnetEnv", func() {
|
||||||
|
Context("when flannel subnet env is valid", func() {
|
||||||
|
It("loads flannel subnet config", func() {
|
||||||
|
conf, err := loadFlannelSubnetEnv(subnetFile)
|
||||||
|
Expect(err).ShouldNot(HaveOccurred())
|
||||||
|
Expect(conf.nw.String()).To(Equal("10.1.0.0/16"))
|
||||||
|
Expect(conf.sn.String()).To(Equal("10.1.17.0/24"))
|
||||||
|
var mtu uint = 1472
|
||||||
|
Expect(*conf.mtu).To(Equal(mtu))
|
||||||
|
Expect(*conf.ipmasq).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when flannel subnet env is invalid", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
subnetFile = writeSubnetEnv("foo=bar")
|
||||||
|
})
|
||||||
|
It("returns an error", func() {
|
||||||
|
_, err := loadFlannelSubnetEnv(subnetFile)
|
||||||
|
Expect(err).To(MatchError(ContainSubstring("missing FLANNEL_NETWORK, FLANNEL_SUBNET, FLANNEL_MTU, FLANNEL_IPMASQ")))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
84
plugins/meta/tuning/tuning.go
Normal file
84
plugins/meta/tuning/tuning.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This is a "meta-plugin". It reads in its own netconf, it does not create
|
||||||
|
// any network interface but just changes the network sysctl.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containernetworking/cni/pkg/ns"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TuningConf represents the network tuning configuration.
|
||||||
|
type TuningConf struct {
|
||||||
|
types.NetConf
|
||||||
|
SysCtl map[string]string `json:"sysctl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAdd(args *skel.CmdArgs) error {
|
||||||
|
tuningConf := TuningConf{}
|
||||||
|
if err := json.Unmarshal(args.StdinData, &tuningConf); err != nil {
|
||||||
|
return fmt.Errorf("failed to load netconf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The directory /proc/sys/net is per network namespace. Enter in the
|
||||||
|
// network namespace before writing on it.
|
||||||
|
|
||||||
|
err := ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
||||||
|
for key, value := range tuningConf.SysCtl {
|
||||||
|
fileName := filepath.Join("/proc/sys", strings.Replace(key, ".", "/", -1))
|
||||||
|
fileName = filepath.Clean(fileName)
|
||||||
|
|
||||||
|
// Refuse to modify sysctl parameters that don't belong
|
||||||
|
// to the network subsystem.
|
||||||
|
if !strings.HasPrefix(fileName, "/proc/sys/net/") {
|
||||||
|
return fmt.Errorf("invalid net sysctl key: %q", key)
|
||||||
|
}
|
||||||
|
content := []byte(value)
|
||||||
|
err := ioutil.WriteFile(fileName, content, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := current.Result{}
|
||||||
|
return result.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdDel(args *skel.CmdArgs) error {
|
||||||
|
// TODO: the settings are not reverted to the previous values. Reverting the
|
||||||
|
// settings is not useful when the whole container goes away but it could be
|
||||||
|
// useful in scenarios where plugins are added and removed at runtime.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||||
|
}
|
23
test
23
test
@ -2,32 +2,15 @@
|
|||||||
#
|
#
|
||||||
# Run CNI plugin tests.
|
# Run CNI plugin tests.
|
||||||
#
|
#
|
||||||
# This needs sudo, as we'll be creating net interfaces. It also needs a valid
|
# This needs sudo, as we'll be creating net interfaces.
|
||||||
# CNI_PATH, with at least the ptp and host-local plugins.
|
|
||||||
#
|
#
|
||||||
# You'll probably run it like this:
|
|
||||||
# CNI_PATH=../cni/bin ./test
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
source ./build
|
source ./build
|
||||||
|
|
||||||
if [ -z "$CNI_PATH" ]; then
|
|
||||||
echo "Need a valid CNI_PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that the plugins we need are available
|
|
||||||
if ! PATH=${CNI_PATH} type -P ptp host-local >& /dev/null; then
|
|
||||||
echo '$CNI_PATH must include ptp and host-local'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
#add our build path to CNI_PATH
|
|
||||||
CNI_PATH=$(pwd)/bin:${CNI_PATH}
|
|
||||||
|
|
||||||
echo "Running tests"
|
echo "Running tests"
|
||||||
|
|
||||||
TESTABLE="plugins/sample plugins/main/vlan"
|
TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/meta/flannel plugins/main/vlan plugins/sample"
|
||||||
|
|
||||||
# user has not provided PKG override
|
# user has not provided PKG override
|
||||||
if [ -z "$PKG" ]; then
|
if [ -z "$PKG" ]; then
|
||||||
@ -48,7 +31,7 @@ fi
|
|||||||
split=(${TEST// / })
|
split=(${TEST// / })
|
||||||
TEST=${split[@]/#/${REPO_PATH}/}
|
TEST=${split[@]/#/${REPO_PATH}/}
|
||||||
|
|
||||||
sudo -E bash -c "umask 0; PATH=${GOROOT}/bin:$(pwd)/bin:${CNI_PATH}:${PATH} go test ${TEST}"
|
sudo -E bash -c "umask 0; PATH=${GOROOT}/bin:$(pwd)/bin:${PATH} go test ${TEST}"
|
||||||
|
|
||||||
echo "Checking gofmt..."
|
echo "Checking gofmt..."
|
||||||
fmtRes=$(gofmt -l $FMT)
|
fmtRes=$(gofmt -l $FMT)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user