diff --git a/build b/build index 4e503f56..b1b82a66 100755 --- a/build +++ b/build @@ -15,7 +15,7 @@ export GOPATH=${PWD}/gopath mkdir -p "${PWD}/bin" echo "Building plugins" -PLUGINS="plugins/sample plugins/main/vlan" +PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample" for d in $PLUGINS; do if [ -d "$d" ]; then plugin="$(basename "$d")" diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go new file mode 100644 index 00000000..c6660b7e --- /dev/null +++ b/plugins/ipam/dhcp/daemon.go @@ -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) +} diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go new file mode 100644 index 00000000..a202720d --- /dev/null +++ b/plugins/ipam/dhcp/lease.go @@ -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), + ) +} diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go new file mode 100644 index 00000000..9289957a --- /dev/null +++ b/plugins/ipam/dhcp/main.go @@ -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 +} diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go new file mode 100644 index 00000000..6e2e05c6 --- /dev/null +++ b/plugins/ipam/dhcp/options.go @@ -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") +} diff --git a/plugins/ipam/dhcp/options_test.go b/plugins/ipam/dhcp/options_test.go new file mode 100644 index 00000000..9f2904bc --- /dev/null +++ b/plugins/ipam/dhcp/options_test.go @@ -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) +} diff --git a/plugins/ipam/host-local/backend/allocator/allocator.go b/plugins/ipam/host-local/backend/allocator/allocator.go new file mode 100644 index 00000000..87b4fa19 --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/allocator.go @@ -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 +} diff --git a/plugins/ipam/host-local/backend/allocator/allocator_suite_test.go b/plugins/ipam/host-local/backend/allocator/allocator_suite_test.go new file mode 100644 index 00000000..d98d856d --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/allocator_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allocator_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestAllocator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Allocator Suite") +} diff --git a/plugins/ipam/host-local/backend/allocator/allocator_test.go b/plugins/ipam/host-local/backend/allocator/allocator_test.go new file mode 100644 index 00000000..54ead09e --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/allocator_test.go @@ -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()) + }) + }) +}) diff --git a/plugins/ipam/host-local/backend/allocator/config.go b/plugins/ipam/host-local/backend/allocator/config.go new file mode 100644 index 00000000..8c004489 --- /dev/null +++ b/plugins/ipam/host-local/backend/allocator/config.go @@ -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 +} diff --git a/plugins/ipam/host-local/backend/disk/backend.go b/plugins/ipam/host-local/backend/disk/backend.go new file mode 100644 index 00000000..483103ac --- /dev/null +++ b/plugins/ipam/host-local/backend/disk/backend.go @@ -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 +} diff --git a/plugins/ipam/host-local/backend/disk/lock.go b/plugins/ipam/host-local/backend/disk/lock.go new file mode 100644 index 00000000..72414825 --- /dev/null +++ b/plugins/ipam/host-local/backend/disk/lock.go @@ -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) +} diff --git a/plugins/ipam/host-local/backend/store.go b/plugins/ipam/host-local/backend/store.go new file mode 100644 index 00000000..82ba8693 --- /dev/null +++ b/plugins/ipam/host-local/backend/store.go @@ -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 +} diff --git a/plugins/ipam/host-local/backend/testing/fake_store.go b/plugins/ipam/host-local/backend/testing/fake_store.go new file mode 100644 index 00000000..c35a130c --- /dev/null +++ b/plugins/ipam/host-local/backend/testing/fake_store.go @@ -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 +} diff --git a/plugins/ipam/host-local/dns.go b/plugins/ipam/host-local/dns.go new file mode 100644 index 00000000..1b3975a5 --- /dev/null +++ b/plugins/ipam/host-local/dns.go @@ -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 +} diff --git a/plugins/ipam/host-local/dns_test.go b/plugins/ipam/host-local/dns_test.go new file mode 100644 index 00000000..4f3a05fa --- /dev/null +++ b/plugins/ipam/host-local/dns_test.go @@ -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()) +} diff --git a/plugins/ipam/host-local/host_local_suite_test.go b/plugins/ipam/host-local/host_local_suite_test.go new file mode 100644 index 00000000..1867ef77 --- /dev/null +++ b/plugins/ipam/host-local/host_local_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestHostLocal(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "HostLocal Suite") +} diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go new file mode 100644 index 00000000..f1578d09 --- /dev/null +++ b/plugins/ipam/host-local/host_local_test.go @@ -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)) + }) +}) diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go new file mode 100644 index 00000000..4db9a071 --- /dev/null +++ b/plugins/ipam/host-local/main.go @@ -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) +} diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go new file mode 100644 index 00000000..876cb5bb --- /dev/null +++ b/plugins/main/bridge/bridge.go @@ -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) +} diff --git a/plugins/main/bridge/bridge_suite_test.go b/plugins/main/bridge/bridge_suite_test.go new file mode 100644 index 00000000..9aa214e4 --- /dev/null +++ b/plugins/main/bridge/bridge_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestBridge(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "bridge Suite") +} diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go new file mode 100644 index 00000000..65f083b7 --- /dev/null +++ b/plugins/main/bridge/bridge_test.go @@ -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()) + }) +}) diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go new file mode 100644 index 00000000..4fba4327 --- /dev/null +++ b/plugins/main/ipvlan/ipvlan.go @@ -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) +} diff --git a/plugins/main/ipvlan/ipvlan_suite_test.go b/plugins/main/ipvlan/ipvlan_suite_test.go new file mode 100644 index 00000000..6ed98ddd --- /dev/null +++ b/plugins/main/ipvlan/ipvlan_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIpvlan(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ipvlan Suite") +} diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go new file mode 100644 index 00000000..41a56256 --- /dev/null +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -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()) + }) +}) diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go new file mode 100644 index 00000000..6441ba5c --- /dev/null +++ b/plugins/main/loopback/loopback.go @@ -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) +} diff --git a/plugins/main/loopback/loopback_suite_test.go b/plugins/main/loopback/loopback_suite_test.go new file mode 100644 index 00000000..212bf601 --- /dev/null +++ b/plugins/main/loopback/loopback_suite_test.go @@ -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() +}) diff --git a/plugins/main/loopback/loopback_test.go b/plugins/main/loopback/loopback_test.go new file mode 100644 index 00000000..f71595b0 --- /dev/null +++ b/plugins/main/loopback/loopback_test.go @@ -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)) + }) + }) +}) diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go new file mode 100644 index 00000000..0d34a4c7 --- /dev/null +++ b/plugins/main/macvlan/macvlan.go @@ -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) +} diff --git a/plugins/main/macvlan/macvlan_suite_test.go b/plugins/main/macvlan/macvlan_suite_test.go new file mode 100644 index 00000000..844e567f --- /dev/null +++ b/plugins/main/macvlan/macvlan_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestMacvlan(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "macvlan Suite") +} diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go new file mode 100644 index 00000000..e17b14f3 --- /dev/null +++ b/plugins/main/macvlan/macvlan_test.go @@ -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()) + + }) +}) diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go new file mode 100644 index 00000000..f90bafbc --- /dev/null +++ b/plugins/main/ptp/ptp.go @@ -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) +} diff --git a/plugins/main/ptp/ptp_suite_test.go b/plugins/main/ptp/ptp_suite_test.go new file mode 100644 index 00000000..05486618 --- /dev/null +++ b/plugins/main/ptp/ptp_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPtp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ptp Suite") +} diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go new file mode 100644 index 00000000..b30fd31c --- /dev/null +++ b/plugins/main/ptp/ptp_test.go @@ -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()) + }) +}) diff --git a/plugins/meta/flannel/flannel.go b/plugins/meta/flannel/flannel.go new file mode 100644 index 00000000..424e01be --- /dev/null +++ b/plugins/meta/flannel/flannel.go @@ -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) +} diff --git a/plugins/meta/flannel/flannel_suite_test.go b/plugins/meta/flannel/flannel_suite_test.go new file mode 100644 index 00000000..ccdffde5 --- /dev/null +++ b/plugins/meta/flannel/flannel_suite_test.go @@ -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") +} diff --git a/plugins/meta/flannel/flannel_test.go b/plugins/meta/flannel/flannel_test.go new file mode 100644 index 00000000..a4a8bc7b --- /dev/null +++ b/plugins/meta/flannel/flannel_test.go @@ -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"))) + }) + }) + }) + }) +}) diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go new file mode 100644 index 00000000..9d8e9002 --- /dev/null +++ b/plugins/meta/tuning/tuning.go @@ -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) +} diff --git a/test b/test index 6c1ec4b8..67e6780d 100755 --- a/test +++ b/test @@ -2,32 +2,15 @@ # # Run CNI plugin tests. # -# This needs sudo, as we'll be creating net interfaces. It also needs a valid -# CNI_PATH, with at least the ptp and host-local plugins. +# This needs sudo, as we'll be creating net interfaces. # -# You'll probably run it like this: -# CNI_PATH=../cni/bin ./test set -e 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" -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 if [ -z "$PKG" ]; then @@ -48,7 +31,7 @@ fi split=(${TEST// / }) 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..." fmtRes=$(gofmt -l $FMT)