diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index c0caa006..16d8fa4c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -9,63 +9,86 @@ { "ImportPath": "github.com/containernetworking/cni/pkg/invoke", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/ip", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/ipam", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/ns", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/skel", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/testutils", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types/020", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types/current", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" + }, + { + "ImportPath": "github.com/containernetworking/cni/pkg/utils", + "Comment": "v0.5.0", + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/utils/hwaddr", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" + }, + { + "ImportPath": "github.com/containernetworking/cni/pkg/utils/sysctl", + "Comment": "v0.5.0", + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/containernetworking/cni/pkg/version", "Comment": "v0.5.0", - "Rev": "4ce9b019aab51b28a32ff6549784a69f9b209fe4" + "Rev": "1a9288c3c09cea4e580fdb1a636f1c5e185a391f" }, { "ImportPath": "github.com/coreos/go-iptables/iptables", "Comment": "v0.1.0", "Rev": "fbb73372b87f6e89951c2b6b31470c2c9d5cfae3" }, + { + "ImportPath": "github.com/coreos/go-systemd/activation", + "Comment": "v2-53-g2688e91", + "Rev": "2688e91251d9d8e404e86dd8f096e23b2f086958" + }, + { + "ImportPath": "github.com/d2g/dhcp4", + "Rev": "f0e4d29ff0231dce36e250b2ed9ff08412584bca" + }, + { + "ImportPath": "github.com/d2g/dhcp4client", + "Rev": "bed07e1bc5b85f69c6f0fd73393aa35ec68ed892" + }, { "ImportPath": "github.com/onsi/ginkgo", "Comment": "v1.2.0-29-g7f8ab55", @@ -151,6 +174,16 @@ "Comment": "v1.0-71-g2152b45", "Rev": "2152b45fa28a361beba9aab0885972323a444e28" }, + { + "ImportPath": "github.com/onsi/gomega/gbytes", + "Comment": "v1.0-71-g2152b45", + "Rev": "2152b45fa28a361beba9aab0885972323a444e28" + }, + { + "ImportPath": "github.com/onsi/gomega/gexec", + "Comment": "v1.0-71-g2152b45", + "Rev": "2152b45fa28a361beba9aab0885972323a444e28" + }, { "ImportPath": "github.com/onsi/gomega/internal/assertion", "Comment": "v1.0-71-g2152b45", diff --git a/build b/build index 14d8abed..b1b82a66 100755 --- a/build +++ b/build @@ -15,7 +15,7 @@ export GOPATH=${PWD}/gopath mkdir -p "${PWD}/bin" echo "Building plugins" -PLUGINS="plugins/*" +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/vlan/vlan.go b/plugins/main/vlan/vlan.go similarity index 100% rename from plugins/vlan/vlan.go rename to plugins/main/vlan/vlan.go diff --git a/plugins/vlan/vlan_suite_test.go b/plugins/main/vlan/vlan_suite_test.go similarity index 100% rename from plugins/vlan/vlan_suite_test.go rename to plugins/main/vlan/vlan_suite_test.go diff --git a/plugins/vlan/vlan_test.go b/plugins/main/vlan/vlan_test.go similarity index 100% rename from plugins/vlan/vlan_test.go rename to plugins/main/vlan/vlan_test.go 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 342d4c6f..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/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) diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/exec_test.go b/vendor/github.com/containernetworking/cni/pkg/invoke/exec_test.go index 7e804ab7..33ffc2de 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/exec_test.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/exec_test.go @@ -50,7 +50,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { VersionDecoder: versionDecoder, } pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`) + netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.1" }`) cniargs = &fakes.CNIArgs{} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec_test.go b/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec_test.go index 5ab23ae5..5d759f24 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec_test.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec_test.go @@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() { "CNI_PATH=/some/bin/path", "CNI_IFNAME=some-eth0", } - stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`) + stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.1"}`) execer = &invoke.RawExec{} }) diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/link.go b/vendor/github.com/containernetworking/cni/pkg/ip/link.go index 6431bb41..a9842627 100644 --- a/vendor/github.com/containernetworking/cni/pkg/ip/link.go +++ b/vendor/github.com/containernetworking/cni/pkg/ip/link.go @@ -16,6 +16,7 @@ package ip import ( "crypto/rand" + "errors" "fmt" "net" "os" @@ -25,6 +26,10 @@ import ( "github.com/vishvananda/netlink" ) +var ( + ErrLinkNotFound = errors.New("link not found") +) + func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { veth := &netlink.Veth{ LinkAttrs: netlink.LinkAttrs{ @@ -98,30 +103,38 @@ func RenameLink(curName, newName string) error { return err } -// SetupVeth sets up a virtual ethernet link. -// Should be in container netns, and will switch back to hostNS to set the host -// veth end up. -func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) { - var hostVethName string - hostVethName, contVeth, err = makeVeth(contVethName, mtu) +func ifaceFromNetlinkLink(l netlink.Link) net.Interface { + a := l.Attrs() + return net.Interface{ + Index: a.Index, + MTU: a.MTU, + Name: a.Name, + HardwareAddr: a.HardwareAddr, + Flags: a.Flags, + } +} + +// SetupVeth sets up a pair of virtual ethernet devices. +// Call SetupVeth from inside the container netns. It will create both veth +// devices and move the host-side veth into the provided hostNS namespace. +// On success, SetupVeth returns (hostVeth, containerVeth, nil) +func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) { + hostVethName, contVeth, err := makeVeth(contVethName, mtu) if err != nil { - return + return net.Interface{}, net.Interface{}, err } if err = netlink.LinkSetUp(contVeth); err != nil { - err = fmt.Errorf("failed to set %q up: %v", contVethName, err) - return + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err) } - hostVeth, err = netlink.LinkByName(hostVethName) + hostVeth, err := netlink.LinkByName(hostVethName) if err != nil { - err = fmt.Errorf("failed to lookup %q: %v", hostVethName, err) - return + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) } if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { - err = fmt.Errorf("failed to move veth to host netns: %v", err) - return + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err) } err = hostNS.Do(func(_ ns.NetNS) error { @@ -135,7 +148,10 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVet } return nil }) - return + if err != nil { + return net.Interface{}, net.Interface{}, err + } + return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil } // DelLinkByName removes an interface link. @@ -157,6 +173,9 @@ func DelLinkByName(ifName string) error { func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { iface, err := netlink.LinkByName(ifName) if err != nil { + if err != nil && err.Error() == "Link not found" { + return nil, ErrLinkNotFound + } return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) } diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/link_test.go b/vendor/github.com/containernetworking/cni/pkg/ip/link_test.go index 3df9ab8f..23182a54 100644 --- a/vendor/github.com/containernetworking/cni/pkg/ip/link_test.go +++ b/vendor/github.com/containernetworking/cni/pkg/ip/link_test.go @@ -46,8 +46,8 @@ var _ = Describe("Link", func() { hostNetNS ns.NetNS containerNetNS ns.NetNS ifaceCounter int = 0 - hostVeth netlink.Link - containerVeth netlink.Link + hostVeth net.Interface + containerVeth net.Interface hostVethName string containerVethName string @@ -78,8 +78,8 @@ var _ = Describe("Link", func() { } Expect(err).NotTo(HaveOccurred()) - hostVethName = hostVeth.Attrs().Name - containerVethName = containerVeth.Attrs().Name + hostVethName = hostVeth.Name + containerVethName = containerVeth.Name return nil }) @@ -98,7 +98,7 @@ var _ = Describe("Link", func() { containerVethFromName, err := netlink.LinkByName(containerVethName) Expect(err).NotTo(HaveOccurred()) - Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Attrs().Index)) + Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Index)) return nil }) @@ -108,7 +108,7 @@ var _ = Describe("Link", func() { hostVethFromName, err := netlink.LinkByName(hostVethName) Expect(err).NotTo(HaveOccurred()) - Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Attrs().Index)) + Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Index)) return nil }) @@ -127,6 +127,20 @@ var _ = Describe("Link", func() { }) }) + Context("deleting an non-existent device", func() { + It("returns known error", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // This string should match the expected error codes in the cmdDel functions of some of the plugins + _, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST", netlink.FAMILY_V4) + Expect(err).To(Equal(ip.ErrLinkNotFound)) + + return nil + }) + }) + }) + Context("when there is no name available for the host-side", func() { BeforeEach(func() { //adding different interface to container ns @@ -156,7 +170,7 @@ var _ = Describe("Link", func() { hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS) Expect(err).NotTo(HaveOccurred()) - hostVethName = hostVeth.Attrs().Name + hostVethName = hostVeth.Name return nil }) diff --git a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go index 666cfe93..2833aba7 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go @@ -23,9 +23,9 @@ import ( "github.com/containernetworking/cni/pkg/types" ) -const implementedSpecVersion string = "0.2.0" +const ImplementedSpecVersion string = "0.2.0" -var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} +var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion} // Compatibility types for CNI version 0.1.0 and 0.2.0 @@ -39,7 +39,7 @@ func NewResult(data []byte) (types.Result, error) { func GetResult(r types.Result) (*Result, error) { // We expect version 0.1.0/0.2.0 results - result020, err := r.GetAsVersion(implementedSpecVersion) + result020, err := r.GetAsVersion(ImplementedSpecVersion) if err != nil { return nil, err } @@ -52,18 +52,20 @@ func GetResult(r types.Result) (*Result, error) { // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { - IP4 *IPConfig `json:"ip4,omitempty"` - IP6 *IPConfig `json:"ip6,omitempty"` - DNS types.DNS `json:"dns,omitempty"` + CNIVersion string `json:"cniVersion,omitempty"` + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` + DNS types.DNS `json:"dns,omitempty"` } func (r *Result) Version() string { - return implementedSpecVersion + return ImplementedSpecVersion } func (r *Result) GetAsVersion(version string) (types.Result, error) { for _, supportedVersion := range SupportedVersions { if version == supportedVersion { + r.CNIVersion = version return r, nil } } diff --git a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go b/vendor/github.com/containernetworking/cni/pkg/types/current/types.go index e686a9a7..b5715fe6 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/current/types.go @@ -24,9 +24,9 @@ import ( "github.com/containernetworking/cni/pkg/types/020" ) -const implementedSpecVersion string = "0.3.0" +const ImplementedSpecVersion string = "0.3.1" -var SupportedVersions = []string{implementedSpecVersion} +var SupportedVersions = []string{"0.3.0", ImplementedSpecVersion} func NewResult(data []byte) (types.Result, error) { result := &Result{} @@ -37,7 +37,7 @@ func NewResult(data []byte) (types.Result, error) { } func GetResult(r types.Result) (*Result, error) { - resultCurrent, err := r.GetAsVersion(implementedSpecVersion) + resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) if err != nil { return nil, err } @@ -63,8 +63,9 @@ func convertFrom020(result types.Result) (*Result, error) { } newResult := &Result{ - DNS: oldResult.DNS, - Routes: []*types.Route{}, + CNIVersion: ImplementedSpecVersion, + DNS: oldResult.DNS, + Routes: []*types.Route{}, } if oldResult.IP4 != nil { @@ -117,6 +118,7 @@ func convertFrom030(result types.Result) (*Result, error) { if !ok { return nil, fmt.Errorf("failed to convert result") } + newResult.CNIVersion = ImplementedSpecVersion return newResult, nil } @@ -129,11 +131,12 @@ func NewResultFromResult(result types.Result) (*Result, error) { } } } - return nil, fmt.Errorf("unsupported CNI result version %q", version) + return nil, fmt.Errorf("unsupported CNI result22 version %q", version) } // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { + CNIVersion string `json:"cniVersion,omitempty"` Interfaces []*Interface `json:"interfaces,omitempty"` IPs []*IPConfig `json:"ips,omitempty"` Routes []*types.Route `json:"routes,omitempty"` @@ -143,7 +146,8 @@ type Result struct { // Convert to the older 0.2.0 CNI spec Result type func (r *Result) convertTo020() (*types020.Result, error) { oldResult := &types020.Result{ - DNS: r.DNS, + CNIVersion: types020.ImplementedSpecVersion, + DNS: r.DNS, } for _, ip := range r.IPs { @@ -189,17 +193,18 @@ func (r *Result) convertTo020() (*types020.Result, error) { } func (r *Result) Version() string { - return implementedSpecVersion + return ImplementedSpecVersion } func (r *Result) GetAsVersion(version string) (types.Result, error) { switch version { - case implementedSpecVersion: + case "0.3.0", ImplementedSpecVersion: + r.CNIVersion = version return r, nil case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: return r.convertTo020() } - return nil, fmt.Errorf("cannot convert version 0.3.0 to %q", version) + return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version) } func (r *Result) Print() error { diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go b/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go new file mode 100644 index 00000000..fe06d2d9 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go @@ -0,0 +1,56 @@ +// 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 sysctl + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" +) + +// Sysctl provides a method to set/get values from /proc/sys - in linux systems +// new interface to set/get values of variables formerly handled by sysctl syscall +// If optional `params` have only one string value - this function will +// set this value into corresponding sysctl variable +func Sysctl(name string, params ...string) (string, error) { + if len(params) > 1 { + return "", fmt.Errorf("unexcepted additional parameters") + } else if len(params) == 1 { + return setSysctl(name, params[0]) + } + return getSysctl(name) +} + +func getSysctl(name string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + data, err := ioutil.ReadFile(fullName) + if err != nil { + return "", err + } + + return string(data[:len(data)-1]), nil +} + +func setSysctl(name, value string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil { + return "", err + } + + return getSysctl(name) +} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/utils.go b/vendor/github.com/containernetworking/cni/pkg/utils/utils.go new file mode 100644 index 00000000..33a2aa79 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/utils/utils.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 utils + +import ( + "crypto/sha512" + "fmt" +) + +const ( + maxChainLength = 28 + chainPrefix = "CNI-" + prefixLength = len(chainPrefix) +) + +// Generates a chain name to be used with iptables. +// Ensures that the generated chain name is exactly +// maxChainLength chars in length +func FormatChainName(name string, id string) string { + chainBytes := sha512.Sum512([]byte(name + id)) + chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes) + return chain[:maxChainLength] +} + +// FormatComment returns a comment used for easier +// rule identification within iptables. +func FormatComment(name string, id string) string { + return fmt.Sprintf("name: %q id: %q", name, id) +} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/utils_suite_test.go b/vendor/github.com/containernetworking/cni/pkg/utils/utils_suite_test.go new file mode 100644 index 00000000..ee614a70 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/utils/utils_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 utils_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/utils_test.go b/vendor/github.com/containernetworking/cni/pkg/utils/utils_test.go new file mode 100644 index 00000000..d703de42 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/utils/utils_test.go @@ -0,0 +1,51 @@ +// 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 utils + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Utils", func() { + It("must format a short name", func() { + chain := FormatChainName("test", "1234") + Expect(len(chain)).To(Equal(maxChainLength)) + Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8")) + }) + + It("must truncate a long name", func() { + chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + Expect(len(chain)).To(Equal(maxChainLength)) + Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe3")) + }) + + It("must be predictable", func() { + chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + Expect(len(chain1)).To(Equal(maxChainLength)) + Expect(len(chain2)).To(Equal(maxChainLength)) + Expect(chain1).To(Equal(chain2)) + }) + + It("must change when a character changes", func() { + chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235") + Expect(len(chain1)).To(Equal(maxChainLength)) + Expect(len(chain2)).To(Equal(maxChainLength)) + Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe3")) + Expect(chain1).NotTo(Equal(chain2)) + }) +}) diff --git a/vendor/github.com/containernetworking/cni/pkg/version/version.go b/vendor/github.com/containernetworking/cni/pkg/version/version.go index 7c589633..efe8ea87 100644 --- a/vendor/github.com/containernetworking/cni/pkg/version/version.go +++ b/vendor/github.com/containernetworking/cni/pkg/version/version.go @@ -24,7 +24,7 @@ import ( // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.3.0" + return "0.3.1" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -35,7 +35,7 @@ func Current() string { // Any future CNI spec versions which meet this definition should be added to // this list. var Legacy = PluginSupports("0.1.0", "0.2.0") -var All = PluginSupports("0.1.0", "0.2.0", "0.3.0") +var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1") var resultFactories = []struct { supportedVersions []string diff --git a/vendor/github.com/coreos/go-systemd/LICENSE b/vendor/github.com/coreos/go-systemd/LICENSE new file mode 100644 index 00000000..37ec93a1 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/coreos/go-systemd/activation/files.go b/vendor/github.com/coreos/go-systemd/activation/files.go new file mode 100644 index 00000000..c8e85fcd --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/files.go @@ -0,0 +1,52 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 activation implements primitives for systemd socket activation. +package activation + +import ( + "os" + "strconv" + "syscall" +) + +// based on: https://gist.github.com/alberts/4640792 +const ( + listenFdsStart = 3 +) + +func Files(unsetEnv bool) []*os.File { + if unsetEnv { + defer os.Unsetenv("LISTEN_PID") + defer os.Unsetenv("LISTEN_FDS") + } + + pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) + if err != nil || pid != os.Getpid() { + return nil + } + + nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) + if err != nil || nfds == 0 { + return nil + } + + files := make([]*os.File, 0, nfds) + for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ { + syscall.CloseOnExec(fd) + files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd))) + } + + return files +} diff --git a/vendor/github.com/coreos/go-systemd/activation/listeners.go b/vendor/github.com/coreos/go-systemd/activation/listeners.go new file mode 100644 index 00000000..a30cb893 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/listeners.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 activation + +import ( + "net" +) + +// Listeners returns a slice containing a net.Listener for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener} +func Listeners(unsetEnv bool) ([]net.Listener, error) { + files := Files(unsetEnv) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[i] = pc + } + } + return listeners, nil +} diff --git a/vendor/github.com/coreos/go-systemd/activation/packetconns.go b/vendor/github.com/coreos/go-systemd/activation/packetconns.go new file mode 100644 index 00000000..48b2ca02 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/packetconns.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 activation + +import ( + "net" +) + +// PacketConns returns a slice containing a net.PacketConn for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn} +func PacketConns(unsetEnv bool) ([]net.PacketConn, error) { + files := Files(unsetEnv) + conns := make([]net.PacketConn, len(files)) + + for i, f := range files { + if pc, err := net.FilePacketConn(f); err == nil { + conns[i] = pc + } + } + return conns, nil +} diff --git a/vendor/github.com/d2g/dhcp4/LICENSE b/vendor/github.com/d2g/dhcp4/LICENSE new file mode 100644 index 00000000..f7d058a3 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Skagerrak Software Limited. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Skagerrak Software Limited nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/github.com/d2g/dhcp4/README.md b/vendor/github.com/d2g/dhcp4/README.md new file mode 100644 index 00000000..6752dc71 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/README.md @@ -0,0 +1,5 @@ +# DHCP4 - A DHCP library written in Go. + +Warning: This library is still being developed. Function calls will change. + +I've removed Server Functionality, for me this project supports the underlying DHCP format not the implementation. diff --git a/vendor/github.com/d2g/dhcp4/constants.go b/vendor/github.com/d2g/dhcp4/constants.go new file mode 100644 index 00000000..183a7783 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/constants.go @@ -0,0 +1,121 @@ +package dhcp4 + +// OpCodes +const ( + BootRequest OpCode = 1 // From Client + BootReply OpCode = 2 // From Server +) + +// DHCP Message Type 53 +const ( + Discover MessageType = 1 // Broadcast Packet From Client - Can I have an IP? + Offer MessageType = 2 // Broadcast From Server - Here's an IP + Request MessageType = 3 // Broadcast From Client - I'll take that IP (Also start for renewals) + Decline MessageType = 4 // Broadcast From Client - Sorry I can't use that IP + ACK MessageType = 5 // From Server, Yes you can have that IP + NAK MessageType = 6 // From Server, No you cannot have that IP + Release MessageType = 7 // From Client, I don't need that IP anymore + Inform MessageType = 8 // From Client, I have this IP and there's nothing you can do about it +) + +// DHCP Options +const ( + End OptionCode = 255 + Pad OptionCode = 0 + OptionSubnetMask OptionCode = 1 + OptionTimeOffset OptionCode = 2 + OptionRouter OptionCode = 3 + OptionTimeServer OptionCode = 4 + OptionNameServer OptionCode = 5 + OptionDomainNameServer OptionCode = 6 + OptionLogServer OptionCode = 7 + OptionCookieServer OptionCode = 8 + OptionLPRServer OptionCode = 9 + OptionImpressServer OptionCode = 10 + OptionResourceLocationServer OptionCode = 11 + OptionHostName OptionCode = 12 + OptionBootFileSize OptionCode = 13 + OptionMeritDumpFile OptionCode = 14 + OptionDomainName OptionCode = 15 + OptionSwapServer OptionCode = 16 + OptionRootPath OptionCode = 17 + OptionExtensionsPath OptionCode = 18 + + // IP Layer Parameters per Host + OptionIPForwardingEnableDisable OptionCode = 19 + OptionNonLocalSourceRoutingEnableDisable OptionCode = 20 + OptionPolicyFilter OptionCode = 21 + OptionMaximumDatagramReassemblySize OptionCode = 22 + OptionDefaultIPTimeToLive OptionCode = 23 + OptionPathMTUAgingTimeout OptionCode = 24 + OptionPathMTUPlateauTable OptionCode = 25 + + // IP Layer Parameters per Interface + OptionInterfaceMTU OptionCode = 26 + OptionAllSubnetsAreLocal OptionCode = 27 + OptionBroadcastAddress OptionCode = 28 + OptionPerformMaskDiscovery OptionCode = 29 + OptionMaskSupplier OptionCode = 30 + OptionPerformRouterDiscovery OptionCode = 31 + OptionRouterSolicitationAddress OptionCode = 32 + OptionStaticRoute OptionCode = 33 + + // Link Layer Parameters per Interface + OptionTrailerEncapsulation OptionCode = 34 + OptionARPCacheTimeout OptionCode = 35 + OptionEthernetEncapsulation OptionCode = 36 + + // TCP Parameters + OptionTCPDefaultTTL OptionCode = 37 + OptionTCPKeepaliveInterval OptionCode = 38 + OptionTCPKeepaliveGarbage OptionCode = 39 + + // Application and Service Parameters + OptionNetworkInformationServiceDomain OptionCode = 40 + OptionNetworkInformationServers OptionCode = 41 + OptionNetworkTimeProtocolServers OptionCode = 42 + OptionVendorSpecificInformation OptionCode = 43 + OptionNetBIOSOverTCPIPNameServer OptionCode = 44 + OptionNetBIOSOverTCPIPDatagramDistributionServer OptionCode = 45 + OptionNetBIOSOverTCPIPNodeType OptionCode = 46 + OptionNetBIOSOverTCPIPScope OptionCode = 47 + OptionXWindowSystemFontServer OptionCode = 48 + OptionXWindowSystemDisplayManager OptionCode = 49 + OptionNetworkInformationServicePlusDomain OptionCode = 64 + OptionNetworkInformationServicePlusServers OptionCode = 65 + OptionMobileIPHomeAgent OptionCode = 68 + OptionSimpleMailTransportProtocol OptionCode = 69 + OptionPostOfficeProtocolServer OptionCode = 70 + OptionNetworkNewsTransportProtocol OptionCode = 71 + OptionDefaultWorldWideWebServer OptionCode = 72 + OptionDefaultFingerServer OptionCode = 73 + OptionDefaultInternetRelayChatServer OptionCode = 74 + OptionStreetTalkServer OptionCode = 75 + OptionStreetTalkDirectoryAssistance OptionCode = 76 + + // DHCP Extensions + OptionRequestedIPAddress OptionCode = 50 + OptionIPAddressLeaseTime OptionCode = 51 + OptionOverload OptionCode = 52 + OptionDHCPMessageType OptionCode = 53 + OptionServerIdentifier OptionCode = 54 + OptionParameterRequestList OptionCode = 55 + OptionMessage OptionCode = 56 + OptionMaximumDHCPMessageSize OptionCode = 57 + OptionRenewalTimeValue OptionCode = 58 + OptionRebindingTimeValue OptionCode = 59 + OptionVendorClassIdentifier OptionCode = 60 + OptionClientIdentifier OptionCode = 61 + + OptionTFTPServerName OptionCode = 66 + OptionBootFileName OptionCode = 67 + + OptionUserClass OptionCode = 77 + + OptionClientArchitecture OptionCode = 93 + + OptionTZPOSIXString OptionCode = 100 + OptionTZDatabaseString OptionCode = 101 + + OptionClasslessRouteFormat OptionCode = 121 +) diff --git a/vendor/github.com/d2g/dhcp4/helpers.go b/vendor/github.com/d2g/dhcp4/helpers.go new file mode 100644 index 00000000..4b146386 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/helpers.go @@ -0,0 +1,58 @@ +package dhcp4 + +import ( + "encoding/binary" + "net" + "time" +) + +// IPRange returns how many ips in the ip range from start to stop (inclusive) +func IPRange(start, stop net.IP) int { + //return int(Uint([]byte(stop))-Uint([]byte(start))) + 1 + return int(binary.BigEndian.Uint32(stop.To4())) - int(binary.BigEndian.Uint32(start.To4())) + 1 +} + +// IPAdd returns a copy of start + add. +// IPAdd(net.IP{192,168,1,1},30) returns net.IP{192.168.1.31} +func IPAdd(start net.IP, add int) net.IP { // IPv4 only + start = start.To4() + //v := Uvarint([]byte(start)) + result := make(net.IP, 4) + binary.BigEndian.PutUint32(result, binary.BigEndian.Uint32(start)+uint32(add)) + //PutUint([]byte(result), v+uint64(add)) + return result +} + +// IPLess returns where IP a is less than IP b. +func IPLess(a, b net.IP) bool { + b = b.To4() + for i, ai := range a.To4() { + if ai != b[i] { + return ai < b[i] + } + } + return false +} + +// IPInRange returns true if ip is between (inclusive) start and stop. +func IPInRange(start, stop, ip net.IP) bool { + return !(IPLess(ip, start) || IPLess(stop, ip)) +} + +// OptionsLeaseTime - converts a time.Duration to a 4 byte slice, compatible +// with OptionIPAddressLeaseTime. +func OptionsLeaseTime(d time.Duration) []byte { + leaseBytes := make([]byte, 4) + binary.BigEndian.PutUint32(leaseBytes, uint32(d/time.Second)) + //PutUvarint(leaseBytes, uint64(d/time.Second)) + return leaseBytes +} + +// JoinIPs returns a byte slice of IP addresses, one immediately after the other +// This may be useful for creating multiple IP options such as OptionRouter. +func JoinIPs(ips []net.IP) (b []byte) { + for _, v := range ips { + b = append(b, v.To4()...) + } + return +} diff --git a/vendor/github.com/d2g/dhcp4/option.go b/vendor/github.com/d2g/dhcp4/option.go new file mode 100644 index 00000000..f3239e19 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/option.go @@ -0,0 +1,40 @@ +package dhcp4 + +type OptionCode byte + +type Option struct { + Code OptionCode + Value []byte +} + +// Map of DHCP options +type Options map[OptionCode][]byte + +// SelectOrderOrAll has same functionality as SelectOrder, except if the order +// param is nil, whereby all options are added (in arbitary order). +func (o Options) SelectOrderOrAll(order []byte) []Option { + if order == nil { + opts := make([]Option, 0, len(o)) + for i, v := range o { + opts = append(opts, Option{Code: i, Value: v}) + } + return opts + } + return o.SelectOrder(order) +} + +// SelectOrder returns a slice of options ordered and selected by a byte array +// usually defined by OptionParameterRequestList. This result is expected to be +// used in ReplyPacket()'s []Option parameter. +func (o Options) SelectOrder(order []byte) []Option { + opts := make([]Option, 0, len(order)) + for _, v := range order { + if data, ok := o[OptionCode(v)]; ok { + opts = append(opts, Option{Code: OptionCode(v), Value: data}) + } + } + return opts +} + +type OpCode byte +type MessageType byte // Option 53 diff --git a/vendor/github.com/d2g/dhcp4/packet.go b/vendor/github.com/d2g/dhcp4/packet.go new file mode 100644 index 00000000..5e547c86 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4/packet.go @@ -0,0 +1,149 @@ +package dhcp4 + +import ( + "net" + "time" +) + +// A DHCP packet +type Packet []byte + +func (p Packet) OpCode() OpCode { return OpCode(p[0]) } +func (p Packet) HType() byte { return p[1] } +func (p Packet) HLen() byte { return p[2] } +func (p Packet) Hops() byte { return p[3] } +func (p Packet) XId() []byte { return p[4:8] } +func (p Packet) Secs() []byte { return p[8:10] } // Never Used? +func (p Packet) Flags() []byte { return p[10:12] } +func (p Packet) CIAddr() net.IP { return net.IP(p[12:16]) } +func (p Packet) YIAddr() net.IP { return net.IP(p[16:20]) } +func (p Packet) SIAddr() net.IP { return net.IP(p[20:24]) } +func (p Packet) GIAddr() net.IP { return net.IP(p[24:28]) } +func (p Packet) CHAddr() net.HardwareAddr { + hLen := p.HLen() + if hLen > 16 { // Prevent chaddr exceeding p boundary + hLen = 16 + } + return net.HardwareAddr(p[28 : 28+hLen]) // max endPos 44 +} + +// 192 bytes of zeros BOOTP legacy +func (p Packet) Cookie() []byte { return p[236:240] } +func (p Packet) Options() []byte { + if len(p) > 240 { + return p[240:] + } + return nil +} + +func (p Packet) Broadcast() bool { return p.Flags()[0] > 127 } + +func (p Packet) SetBroadcast(broadcast bool) { + if p.Broadcast() != broadcast { + p.Flags()[0] ^= 128 + } +} + +func (p Packet) SetOpCode(c OpCode) { p[0] = byte(c) } +func (p Packet) SetCHAddr(a net.HardwareAddr) { + copy(p[28:44], a) + p[2] = byte(len(a)) +} +func (p Packet) SetHType(hType byte) { p[1] = hType } +func (p Packet) SetCookie(cookie []byte) { copy(p.Cookie(), cookie) } +func (p Packet) SetHops(hops byte) { p[3] = hops } +func (p Packet) SetXId(xId []byte) { copy(p.XId(), xId) } +func (p Packet) SetSecs(secs []byte) { copy(p.Secs(), secs) } +func (p Packet) SetFlags(flags []byte) { copy(p.Flags(), flags) } +func (p Packet) SetCIAddr(ip net.IP) { copy(p.CIAddr(), ip.To4()) } +func (p Packet) SetYIAddr(ip net.IP) { copy(p.YIAddr(), ip.To4()) } +func (p Packet) SetSIAddr(ip net.IP) { copy(p.SIAddr(), ip.To4()) } +func (p Packet) SetGIAddr(ip net.IP) { copy(p.GIAddr(), ip.To4()) } + +// Parses the packet's options into an Options map +func (p Packet) ParseOptions() Options { + opts := p.Options() + options := make(Options, 10) + for len(opts) >= 2 && OptionCode(opts[0]) != End { + if OptionCode(opts[0]) == Pad { + opts = opts[1:] + continue + } + size := int(opts[1]) + if len(opts) < 2+size { + break + } + options[OptionCode(opts[0])] = opts[2 : 2+size] + opts = opts[2+size:] + } + return options +} + +func NewPacket(opCode OpCode) Packet { + p := make(Packet, 241) + p.SetOpCode(opCode) + p.SetHType(1) // Ethernet + p.SetCookie([]byte{99, 130, 83, 99}) + p[240] = byte(End) + return p +} + +// Appends a DHCP option to the end of a packet +func (p *Packet) AddOption(o OptionCode, value []byte) { + *p = append((*p)[:len(*p)-1], []byte{byte(o), byte(len(value))}...) // Strip off End, Add OptionCode and Length + *p = append(*p, value...) // Add Option Value + *p = append(*p, byte(End)) // Add on new End +} + +// Removes all options from packet. +func (p *Packet) StripOptions() { + *p = append((*p)[:240], byte(End)) +} + +// Creates a request packet that a Client would send to a server. +func RequestPacket(mt MessageType, chAddr net.HardwareAddr, cIAddr net.IP, xId []byte, broadcast bool, options []Option) Packet { + p := NewPacket(BootRequest) + p.SetCHAddr(chAddr) + p.SetXId(xId) + if cIAddr != nil { + p.SetCIAddr(cIAddr) + } + p.SetBroadcast(broadcast) + p.AddOption(OptionDHCPMessageType, []byte{byte(mt)}) + for _, o := range options { + p.AddOption(o.Code, o.Value) + } + p.PadToMinSize() + return p +} + +// ReplyPacket creates a reply packet that a Server would send to a client. +// It uses the req Packet param to copy across common/necessary fields to +// associate the reply the request. +func ReplyPacket(req Packet, mt MessageType, serverId, yIAddr net.IP, leaseDuration time.Duration, options []Option) Packet { + p := NewPacket(BootReply) + p.SetXId(req.XId()) + p.SetFlags(req.Flags()) + p.SetYIAddr(yIAddr) + p.SetGIAddr(req.GIAddr()) + p.SetCHAddr(req.CHAddr()) + p.SetSecs(req.Secs()) + p.AddOption(OptionDHCPMessageType, []byte{byte(mt)}) + p.AddOption(OptionServerIdentifier, []byte(serverId)) + p.AddOption(OptionIPAddressLeaseTime, OptionsLeaseTime(leaseDuration)) + for _, o := range options { + p.AddOption(o.Code, o.Value) + } + p.PadToMinSize() + return p +} + +// PadToMinSize pads a packet so that when sent over UDP, the entire packet, +// is 300 bytes (BOOTP min), to be compatible with really old devices. +var padder [272]byte + +func (p *Packet) PadToMinSize() { + if n := len(*p); n < 272 { + *p = append(*p, padder[:272-n]...) + } +} diff --git a/vendor/github.com/d2g/dhcp4client/LICENSE b/vendor/github.com/d2g/dhcp4client/LICENSE new file mode 100644 index 00000000..c33dcc7c --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/LICENSE @@ -0,0 +1,354 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/d2g/dhcp4client/README.md b/vendor/github.com/d2g/dhcp4client/README.md new file mode 100644 index 00000000..785ac2c1 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/README.md @@ -0,0 +1,8 @@ +dhcp4client [![GoDoc](https://godoc.org/github.com/d2g/dhcp4client?status.svg)](http://godoc.org/github.com/d2g/dhcp4client) [![Coverage Status](https://coveralls.io/repos/d2g/dhcp4client/badge.svg?branch=HEAD)](https://coveralls.io/r/d2g/dhcp4client?branch=HEAD) [![Codeship Status for d2g/dhcp4client](https://codeship.com/projects/d75d9860-b364-0132-bc79-7e1d8cf367b9/status?branch=master)](https://codeship.com/projects/70187) +=========== + +DHCP Client + + +###### Thanks to: +@eyakubovich For AF_PACKET support. diff --git a/vendor/github.com/d2g/dhcp4client/client.go b/vendor/github.com/d2g/dhcp4client/client.go new file mode 100644 index 00000000..ab852c65 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/client.go @@ -0,0 +1,366 @@ +package dhcp4client + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/d2g/dhcp4" +) + +const ( + MaxDHCPLen = 576 +) + +type Client struct { + hardwareAddr net.HardwareAddr //The HardwareAddr to send in the request. + ignoreServers []net.IP //List of Servers to Ignore requests from. + timeout time.Duration //Time before we timeout. + broadcast bool //Set the Bcast flag in BOOTP Flags + connection connection //The Connection Method to use +} + +/* + * Abstracts the type of underlying socket used + */ +type connection interface { + Close() error + Write(packet []byte) error + ReadFrom() ([]byte, net.IP, error) + SetReadTimeout(t time.Duration) error +} + +func New(options ...func(*Client) error) (*Client, error) { + c := Client{ + timeout: time.Second * 10, + broadcast: true, + } + + err := c.SetOption(options...) + if err != nil { + return nil, err + } + + //if connection hasn't been set as an option create the default. + if c.connection == nil { + conn, err := NewInetSock() + if err != nil { + return nil, err + } + c.connection = conn + } + + return &c, nil +} + +func (c *Client) SetOption(options ...func(*Client) error) error { + for _, opt := range options { + if err := opt(c); err != nil { + return err + } + } + return nil +} + +func Timeout(t time.Duration) func(*Client) error { + return func(c *Client) error { + c.timeout = t + return nil + } +} + +func IgnoreServers(s []net.IP) func(*Client) error { + return func(c *Client) error { + c.ignoreServers = s + return nil + } +} + +func HardwareAddr(h net.HardwareAddr) func(*Client) error { + return func(c *Client) error { + c.hardwareAddr = h + return nil + } +} + +func Broadcast(b bool) func(*Client) error { + return func(c *Client) error { + c.broadcast = b + return nil + } +} + +func Connection(conn connection) func(*Client) error { + return func(c *Client) error { + c.connection = conn + return nil + } +} + +/* + * Close Connections + */ +func (c *Client) Close() error { + if c.connection != nil { + return c.connection.Close() + } + return nil +} + +/* + * Send the Discovery Packet to the Broadcast Channel + */ +func (c *Client) SendDiscoverPacket() (dhcp4.Packet, error) { + discoveryPacket := c.DiscoverPacket() + discoveryPacket.PadToMinSize() + + return discoveryPacket, c.SendPacket(discoveryPacket) +} + +/* + * Retreive Offer... + * Wait for the offer for a specific Discovery Packet. + */ +func (c *Client) GetOffer(discoverPacket *dhcp4.Packet) (dhcp4.Packet, error) { + for { + c.connection.SetReadTimeout(c.timeout) + readBuffer, source, err := c.connection.ReadFrom() + if err != nil { + return dhcp4.Packet{}, err + } + + offerPacket := dhcp4.Packet(readBuffer) + offerPacketOptions := offerPacket.ParseOptions() + + // Ignore Servers in my Ignore list + for _, ignoreServer := range c.ignoreServers { + if source.Equal(ignoreServer) { + continue + } + + if offerPacket.SIAddr().Equal(ignoreServer) { + continue + } + } + + if len(offerPacketOptions[dhcp4.OptionDHCPMessageType]) < 1 || dhcp4.MessageType(offerPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.Offer || !bytes.Equal(discoverPacket.XId(), offerPacket.XId()) { + continue + } + + return offerPacket, nil + } + +} + +/* + * Send Request Based On the offer Received. + */ +func (c *Client) SendRequest(offerPacket *dhcp4.Packet) (dhcp4.Packet, error) { + requestPacket := c.RequestPacket(offerPacket) + requestPacket.PadToMinSize() + + return requestPacket, c.SendPacket(requestPacket) +} + +/* + * Retreive Acknowledgement + * Wait for the offer for a specific Request Packet. + */ +func (c *Client) GetAcknowledgement(requestPacket *dhcp4.Packet) (dhcp4.Packet, error) { + for { + c.connection.SetReadTimeout(c.timeout) + readBuffer, source, err := c.connection.ReadFrom() + if err != nil { + return dhcp4.Packet{}, err + } + + acknowledgementPacket := dhcp4.Packet(readBuffer) + acknowledgementPacketOptions := acknowledgementPacket.ParseOptions() + + // Ignore Servers in my Ignore list + for _, ignoreServer := range c.ignoreServers { + if source.Equal(ignoreServer) { + continue + } + + if acknowledgementPacket.SIAddr().Equal(ignoreServer) { + continue + } + } + + if !bytes.Equal(requestPacket.XId(), acknowledgementPacket.XId()) || len(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType]) < 1 || (dhcp4.MessageType(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK && dhcp4.MessageType(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.NAK) { + continue + } + + return acknowledgementPacket, nil + } +} + +/* + * Send a DHCP Packet. + */ +func (c *Client) SendPacket(packet dhcp4.Packet) error { + return c.connection.Write(packet) +} + +/* + * Create Discover Packet + */ +func (c *Client) DiscoverPacket() dhcp4.Packet { + messageid := make([]byte, 4) + if _, err := rand.Read(messageid); err != nil { + panic(err) + } + + packet := dhcp4.NewPacket(dhcp4.BootRequest) + packet.SetCHAddr(c.hardwareAddr) + packet.SetXId(messageid) + packet.SetBroadcast(c.broadcast) + + packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Discover)}) + //packet.PadToMinSize() + return packet +} + +/* + * Create Request Packet + */ +func (c *Client) RequestPacket(offerPacket *dhcp4.Packet) dhcp4.Packet { + offerOptions := offerPacket.ParseOptions() + + packet := dhcp4.NewPacket(dhcp4.BootRequest) + packet.SetCHAddr(c.hardwareAddr) + + packet.SetXId(offerPacket.XId()) + packet.SetCIAddr(offerPacket.CIAddr()) + packet.SetSIAddr(offerPacket.SIAddr()) + + packet.SetBroadcast(c.broadcast) + packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Request)}) + packet.AddOption(dhcp4.OptionRequestedIPAddress, (offerPacket.YIAddr()).To4()) + packet.AddOption(dhcp4.OptionServerIdentifier, offerOptions[dhcp4.OptionServerIdentifier]) + + //packet.PadToMinSize() + return packet +} + +/* + * Create Request Packet For a Renew + */ +func (c *Client) RenewalRequestPacket(acknowledgement *dhcp4.Packet) dhcp4.Packet { + messageid := make([]byte, 4) + if _, err := rand.Read(messageid); err != nil { + panic(err) + } + + acknowledgementOptions := acknowledgement.ParseOptions() + + packet := dhcp4.NewPacket(dhcp4.BootRequest) + packet.SetCHAddr(acknowledgement.CHAddr()) + + packet.SetXId(messageid) + packet.SetCIAddr(acknowledgement.YIAddr()) + packet.SetSIAddr(acknowledgement.SIAddr()) + + packet.SetBroadcast(c.broadcast) + packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Request)}) + packet.AddOption(dhcp4.OptionRequestedIPAddress, (acknowledgement.YIAddr()).To4()) + packet.AddOption(dhcp4.OptionServerIdentifier, acknowledgementOptions[dhcp4.OptionServerIdentifier]) + + //packet.PadToMinSize() + return packet +} + +/* + * Create Release Packet For a Release + */ +func (c *Client) ReleasePacket(acknowledgement *dhcp4.Packet) dhcp4.Packet { + messageid := make([]byte, 4) + if _, err := rand.Read(messageid); err != nil { + panic(err) + } + + acknowledgementOptions := acknowledgement.ParseOptions() + + packet := dhcp4.NewPacket(dhcp4.BootRequest) + packet.SetCHAddr(acknowledgement.CHAddr()) + + packet.SetXId(messageid) + packet.SetCIAddr(acknowledgement.YIAddr()) + + packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Release)}) + packet.AddOption(dhcp4.OptionServerIdentifier, acknowledgementOptions[dhcp4.OptionServerIdentifier]) + + //packet.PadToMinSize() + return packet +} + +/* + * Lets do a Full DHCP Request. + */ +func (c *Client) Request() (bool, dhcp4.Packet, error) { + discoveryPacket, err := c.SendDiscoverPacket() + if err != nil { + return false, discoveryPacket, err + } + + offerPacket, err := c.GetOffer(&discoveryPacket) + if err != nil { + return false, offerPacket, err + } + + requestPacket, err := c.SendRequest(&offerPacket) + if err != nil { + return false, requestPacket, err + } + + acknowledgement, err := c.GetAcknowledgement(&requestPacket) + if err != nil { + return false, acknowledgement, err + } + + acknowledgementOptions := acknowledgement.ParseOptions() + if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { + return false, acknowledgement, nil + } + + return true, acknowledgement, nil +} + +/* + * Renew a lease backed on the Acknowledgement Packet. + * Returns Sucessfull, The AcknoledgementPacket, Any Errors + */ +func (c *Client) Renew(acknowledgement dhcp4.Packet) (bool, dhcp4.Packet, error) { + renewRequest := c.RenewalRequestPacket(&acknowledgement) + renewRequest.PadToMinSize() + + err := c.SendPacket(renewRequest) + if err != nil { + return false, renewRequest, err + } + + newAcknowledgement, err := c.GetAcknowledgement(&renewRequest) + if err != nil { + return false, newAcknowledgement, err + } + + newAcknowledgementOptions := newAcknowledgement.ParseOptions() + if dhcp4.MessageType(newAcknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { + return false, newAcknowledgement, nil + } + + return true, newAcknowledgement, nil +} + +/* + * Release a lease backed on the Acknowledgement Packet. + * Returns Any Errors + */ +func (c *Client) Release(acknowledgement dhcp4.Packet) error { + release := c.ReleasePacket(&acknowledgement) + release.PadToMinSize() + + return c.SendPacket(release) +} diff --git a/vendor/github.com/d2g/dhcp4client/inetsock.go b/vendor/github.com/d2g/dhcp4client/inetsock.go new file mode 100644 index 00000000..293f1865 --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/inetsock.go @@ -0,0 +1,75 @@ +package dhcp4client + +import ( + "net" + "time" +) + +type inetSock struct { + *net.UDPConn + + laddr net.UDPAddr + raddr net.UDPAddr +} + +func NewInetSock(options ...func(*inetSock) error) (*inetSock, error) { + c := &inetSock{ + laddr: net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 68}, + raddr: net.UDPAddr{IP: net.IPv4bcast, Port: 67}, + } + + err := c.setOption(options...) + if err != nil { + return nil, err + } + + conn, err := net.ListenUDP("udp4", &c.laddr) + if err != nil { + return nil, err + } + + c.UDPConn = conn + return c, err +} + +func (c *inetSock) setOption(options ...func(*inetSock) error) error { + for _, opt := range options { + if err := opt(c); err != nil { + return err + } + } + return nil +} + +func SetLocalAddr(l net.UDPAddr) func(*inetSock) error { + return func(c *inetSock) error { + c.laddr = l + return nil + } +} + +func SetRemoteAddr(r net.UDPAddr) func(*inetSock) error { + return func(c *inetSock) error { + c.raddr = r + return nil + } +} + +func (c *inetSock) Write(packet []byte) error { + _, err := c.WriteToUDP(packet, &c.raddr) + return err +} + +func (c *inetSock) ReadFrom() ([]byte, net.IP, error) { + readBuffer := make([]byte, MaxDHCPLen) + n, source, err := c.ReadFromUDP(readBuffer) + if source != nil { + return readBuffer[:n], source.IP, err + } else { + return readBuffer[:n], net.IP{}, err + } +} + +func (c *inetSock) SetReadTimeout(t time.Duration) error { + return c.SetReadDeadline(time.Now().Add(t)) +} diff --git a/vendor/github.com/d2g/dhcp4client/init.go b/vendor/github.com/d2g/dhcp4client/init.go new file mode 100644 index 00000000..67ae8a3a --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/init.go @@ -0,0 +1,10 @@ +package dhcp4client + +import ( + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} diff --git a/vendor/github.com/d2g/dhcp4client/pktsock_linux.go b/vendor/github.com/d2g/dhcp4client/pktsock_linux.go new file mode 100644 index 00000000..a21c265f --- /dev/null +++ b/vendor/github.com/d2g/dhcp4client/pktsock_linux.go @@ -0,0 +1,147 @@ +package dhcp4client + +import ( + "crypto/rand" + "encoding/binary" + "net" + "time" + + "golang.org/x/sys/unix" +) + +const ( + minIPHdrLen = 20 + maxIPHdrLen = 60 + udpHdrLen = 8 + ip4Ver = 0x40 + ttl = 16 + srcPort = 68 + dstPort = 67 +) + +var ( + bcastMAC = []byte{255, 255, 255, 255, 255, 255} +) + +// abstracts AF_PACKET +type packetSock struct { + fd int + ifindex int +} + +func NewPacketSock(ifindex int) (*packetSock, error) { + fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM, int(swap16(unix.ETH_P_IP))) + if err != nil { + return nil, err + } + + addr := unix.SockaddrLinklayer{ + Ifindex: ifindex, + Protocol: swap16(unix.ETH_P_IP), + } + + if err = unix.Bind(fd, &addr); err != nil { + return nil, err + } + + return &packetSock{ + fd: fd, + ifindex: ifindex, + }, nil +} + +func (pc *packetSock) Close() error { + return unix.Close(pc.fd) +} + +func (pc *packetSock) Write(packet []byte) error { + lladdr := unix.SockaddrLinklayer{ + Ifindex: pc.ifindex, + Protocol: swap16(unix.ETH_P_IP), + Halen: uint8(len(bcastMAC)), + } + copy(lladdr.Addr[:], bcastMAC) + + pkt := make([]byte, minIPHdrLen+udpHdrLen+len(packet)) + + fillIPHdr(pkt[0:minIPHdrLen], udpHdrLen+uint16(len(packet))) + fillUDPHdr(pkt[minIPHdrLen:minIPHdrLen+udpHdrLen], uint16(len(packet))) + + // payload + copy(pkt[minIPHdrLen+udpHdrLen:len(pkt)], packet) + + return unix.Sendto(pc.fd, pkt, 0, &lladdr) +} + +func (pc *packetSock) ReadFrom() ([]byte, net.IP, error) { + pkt := make([]byte, maxIPHdrLen+udpHdrLen+MaxDHCPLen) + n, _, err := unix.Recvfrom(pc.fd, pkt, 0) + if err != nil { + return nil, nil, err + } + + // IP hdr len + ihl := int(pkt[0]&0x0F) * 4 + // Source IP address + src := net.IP(pkt[12:16]) + + return pkt[ihl+udpHdrLen : n], src, nil +} + +func (pc *packetSock) SetReadTimeout(t time.Duration) error { + + tv := unix.NsecToTimeval(t.Nanoseconds()) + return unix.SetsockoptTimeval(pc.fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) +} + +// compute's 1's complement checksum +func chksum(p []byte, csum []byte) { + cklen := len(p) + s := uint32(0) + for i := 0; i < (cklen - 1); i += 2 { + s += uint32(p[i+1])<<8 | uint32(p[i]) + } + if cklen&1 == 1 { + s += uint32(p[cklen-1]) + } + s = (s >> 16) + (s & 0xffff) + s = s + (s >> 16) + s = ^s + + csum[0] = uint8(s & 0xff) + csum[1] = uint8(s >> 8) +} + +func fillIPHdr(hdr []byte, payloadLen uint16) { + // version + IHL + hdr[0] = ip4Ver | (minIPHdrLen / 4) + // total length + binary.BigEndian.PutUint16(hdr[2:4], uint16(len(hdr))+payloadLen) + // identification + if _, err := rand.Read(hdr[4:5]); err != nil { + panic(err) + } + // TTL + hdr[8] = 16 + // Protocol + hdr[9] = unix.IPPROTO_UDP + // dst IP + copy(hdr[16:20], net.IPv4bcast.To4()) + // compute IP hdr checksum + chksum(hdr[0:len(hdr)], hdr[10:12]) +} + +func fillUDPHdr(hdr []byte, payloadLen uint16) { + // src port + binary.BigEndian.PutUint16(hdr[0:2], srcPort) + // dest port + binary.BigEndian.PutUint16(hdr[2:4], dstPort) + // length + binary.BigEndian.PutUint16(hdr[4:6], udpHdrLen+payloadLen) +} + +func swap16(x uint16) uint16 { + var b [2]byte + binary.BigEndian.PutUint16(b[:], x) + return binary.LittleEndian.Uint16(b[:]) +} diff --git a/vendor/github.com/onsi/gomega/gbytes/buffer.go b/vendor/github.com/onsi/gomega/gbytes/buffer.go new file mode 100644 index 00000000..8775b861 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gbytes/buffer.go @@ -0,0 +1,229 @@ +/* +Package gbytes provides a buffer that supports incrementally detecting input. + +You use gbytes.Buffer with the gbytes.Say matcher. When Say finds a match, it fastforwards the buffer's read cursor to the end of that match. + +Subsequent matches against the buffer will only operate against data that appears *after* the read cursor. + +The read cursor is an opaque implementation detail that you cannot access. You should use the Say matcher to sift through the buffer. You can always +access the entire buffer's contents with Contents(). + +*/ +package gbytes + +import ( + "errors" + "fmt" + "io" + "regexp" + "sync" + "time" +) + +/* +gbytes.Buffer implements an io.Writer and can be used with the gbytes.Say matcher. + +You should only use a gbytes.Buffer in test code. It stores all writes in an in-memory buffer - behavior that is inappropriate for production code! +*/ +type Buffer struct { + contents []byte + readCursor uint64 + lock *sync.Mutex + detectCloser chan interface{} + closed bool +} + +/* +NewBuffer returns a new gbytes.Buffer +*/ +func NewBuffer() *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + } +} + +/* +BufferWithBytes returns a new gbytes.Buffer seeded with the passed in bytes +*/ +func BufferWithBytes(bytes []byte) *Buffer { + return &Buffer{ + lock: &sync.Mutex{}, + contents: bytes, + } +} + +/* +Write implements the io.Writer interface +*/ +func (b *Buffer) Write(p []byte) (n int, err error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return 0, errors.New("attempt to write to closed buffer") + } + + b.contents = append(b.contents, p...) + return len(p), nil +} + +/* +Read implements the io.Reader interface. It advances the +cursor as it reads. + +Returns an error if called after Close. +*/ +func (b *Buffer) Read(d []byte) (int, error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return 0, errors.New("attempt to read from closed buffer") + } + + if uint64(len(b.contents)) <= b.readCursor { + return 0, io.EOF + } + + n := copy(d, b.contents[b.readCursor:]) + b.readCursor += uint64(n) + + return n, nil +} + +/* +Close signifies that the buffer will no longer be written to +*/ +func (b *Buffer) Close() error { + b.lock.Lock() + defer b.lock.Unlock() + + b.closed = true + + return nil +} + +/* +Closed returns true if the buffer has been closed +*/ +func (b *Buffer) Closed() bool { + b.lock.Lock() + defer b.lock.Unlock() + + return b.closed +} + +/* +Contents returns all data ever written to the buffer. +*/ +func (b *Buffer) Contents() []byte { + b.lock.Lock() + defer b.lock.Unlock() + + contents := make([]byte, len(b.contents)) + copy(contents, b.contents) + return contents +} + +/* +Detect takes a regular expression and returns a channel. + +The channel will receive true the first time data matching the regular expression is written to the buffer. +The channel is subsequently closed and the buffer's read-cursor is fast-forwarded to just after the matching region. + +You typically don't need to use Detect and should use the ghttp.Say matcher instead. Detect is useful, however, in cases where your code must +be branch and handle different outputs written to the buffer. + +For example, consider a buffer hooked up to the stdout of a client library. You may (or may not, depending on state outside of your control) need to authenticate the client library. + +You could do something like: + +select { +case <-buffer.Detect("You are not logged in"): + //log in +case <-buffer.Detect("Success"): + //carry on +case <-time.After(time.Second): + //welp +} +buffer.CancelDetects() + +You should always call CancelDetects after using Detect. This will close any channels that have not detected and clean up the goroutines that were spawned to support them. + +Finally, you can pass detect a format string followed by variadic arguments. This will construct the regexp using fmt.Sprintf. +*/ +func (b *Buffer) Detect(desired string, args ...interface{}) chan bool { + formattedRegexp := desired + if len(args) > 0 { + formattedRegexp = fmt.Sprintf(desired, args...) + } + re := regexp.MustCompile(formattedRegexp) + + b.lock.Lock() + defer b.lock.Unlock() + + if b.detectCloser == nil { + b.detectCloser = make(chan interface{}) + } + + closer := b.detectCloser + response := make(chan bool) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + defer close(response) + for { + select { + case <-ticker.C: + b.lock.Lock() + data, cursor := b.contents[b.readCursor:], b.readCursor + loc := re.FindIndex(data) + b.lock.Unlock() + + if loc != nil { + response <- true + b.lock.Lock() + newCursorPosition := cursor + uint64(loc[1]) + if newCursorPosition >= b.readCursor { + b.readCursor = newCursorPosition + } + b.lock.Unlock() + return + } + case <-closer: + return + } + } + }() + + return response +} + +/* +CancelDetects cancels any pending detects and cleans up their goroutines. You should always call this when you're done with a set of Detect channels. +*/ +func (b *Buffer) CancelDetects() { + b.lock.Lock() + defer b.lock.Unlock() + + close(b.detectCloser) + b.detectCloser = nil +} + +func (b *Buffer) didSay(re *regexp.Regexp) (bool, []byte) { + b.lock.Lock() + defer b.lock.Unlock() + + unreadBytes := b.contents[b.readCursor:] + copyOfUnreadBytes := make([]byte, len(unreadBytes)) + copy(copyOfUnreadBytes, unreadBytes) + + loc := re.FindIndex(unreadBytes) + + if loc != nil { + b.readCursor += uint64(loc[1]) + return true, copyOfUnreadBytes + } else { + return false, copyOfUnreadBytes + } +} diff --git a/vendor/github.com/onsi/gomega/gbytes/say_matcher.go b/vendor/github.com/onsi/gomega/gbytes/say_matcher.go new file mode 100644 index 00000000..ce5ebcbf --- /dev/null +++ b/vendor/github.com/onsi/gomega/gbytes/say_matcher.go @@ -0,0 +1,105 @@ +package gbytes + +import ( + "fmt" + "regexp" + + "github.com/onsi/gomega/format" +) + +//Objects satisfying the BufferProvider can be used with the Say matcher. +type BufferProvider interface { + Buffer() *Buffer +} + +/* +Say is a Gomega matcher that operates on gbytes.Buffers: + + Ω(buffer).Should(Say("something")) + +will succeed if the unread portion of the buffer matches the regular expression "something". + +When Say succeeds, it fast forwards the gbytes.Buffer's read cursor to just after the succesful match. +Thus, subsequent calls to Say will only match against the unread portion of the buffer + +Say pairs very well with Eventually. To asser that a buffer eventually receives data matching "[123]-star" within 3 seconds you can: + + Eventually(buffer, 3).Should(Say("[123]-star")) + +Ditto with consistently. To assert that a buffer does not receive data matching "never-see-this" for 1 second you can: + + Consistently(buffer, 1).ShouldNot(Say("never-see-this")) + +In addition to bytes.Buffers, Say can operate on objects that implement the gbytes.BufferProvider interface. +In such cases, Say simply operates on the *gbytes.Buffer returned by Buffer() + +If the buffer is closed, the Say matcher will tell Eventually to abort. +*/ +func Say(expected string, args ...interface{}) *sayMatcher { + formattedRegexp := expected + if len(args) > 0 { + formattedRegexp = fmt.Sprintf(expected, args...) + } + return &sayMatcher{ + re: regexp.MustCompile(formattedRegexp), + } +} + +type sayMatcher struct { + re *regexp.Regexp + receivedSayings []byte +} + +func (m *sayMatcher) buffer(actual interface{}) (*Buffer, bool) { + var buffer *Buffer + + switch x := actual.(type) { + case *Buffer: + buffer = x + case BufferProvider: + buffer = x.Buffer() + default: + return nil, false + } + + return buffer, true +} + +func (m *sayMatcher) Match(actual interface{}) (success bool, err error) { + buffer, ok := m.buffer(actual) + if !ok { + return false, fmt.Errorf("Say must be passed a *gbytes.Buffer or BufferProvider. Got:\n%s", format.Object(actual, 1)) + } + + didSay, sayings := buffer.didSay(m.re) + m.receivedSayings = sayings + + return didSay, nil +} + +func (m *sayMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Got stuck at:\n%s\nWaiting for:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf( + "Saw:\n%s\nWhich matches the unexpected:\n%s", + format.IndentString(string(m.receivedSayings), 1), + format.IndentString(m.re.String(), 1), + ) +} + +func (m *sayMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + switch x := actual.(type) { + case *Buffer: + return !x.Closed() + case BufferProvider: + return !x.Buffer().Closed() + default: + return true + } +} diff --git a/vendor/github.com/onsi/gomega/gexec/build.go b/vendor/github.com/onsi/gomega/gexec/build.go new file mode 100644 index 00000000..3e9bf9f9 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/build.go @@ -0,0 +1,78 @@ +package gexec + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" +) + +var tmpDir string + +/* +Build uses go build to compile the package at packagePath. The resulting binary is saved off in a temporary directory. +A path pointing to this binary is returned. + +Build uses the $GOPATH set in your environment. It passes the variadic args on to `go build`. +*/ +func Build(packagePath string, args ...string) (compiledPath string, err error) { + return BuildIn(os.Getenv("GOPATH"), packagePath, args...) +} + +/* +BuildIn is identical to Build but allows you to specify a custom $GOPATH (the first argument). +*/ +func BuildIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) { + tmpDir, err := temporaryDirectory() + if err != nil { + return "", err + } + + if len(gopath) == 0 { + return "", errors.New("$GOPATH not provided when building " + packagePath) + } + + executable := filepath.Join(tmpDir, path.Base(packagePath)) + if runtime.GOOS == "windows" { + executable = executable + ".exe" + } + + cmdArgs := append([]string{"build"}, args...) + cmdArgs = append(cmdArgs, "-o", executable, packagePath) + + build := exec.Command("go", cmdArgs...) + build.Env = append([]string{"GOPATH=" + gopath}, os.Environ()...) + + output, err := build.CombinedOutput() + if err != nil { + return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output)) + } + + return executable, nil +} + +/* +You should call CleanupBuildArtifacts before your test ends to clean up any temporary artifacts generated by +gexec. In Ginkgo this is typically done in an AfterSuite callback. +*/ +func CleanupBuildArtifacts() { + if tmpDir != "" { + os.RemoveAll(tmpDir) + } +} + +func temporaryDirectory() (string, error) { + var err error + if tmpDir == "" { + tmpDir, err = ioutil.TempDir("", "gexec_artifacts") + if err != nil { + return "", err + } + } + + return ioutil.TempDir(tmpDir, "g") +} diff --git a/vendor/github.com/onsi/gomega/gexec/exit_matcher.go b/vendor/github.com/onsi/gomega/gexec/exit_matcher.go new file mode 100644 index 00000000..e6f43294 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/exit_matcher.go @@ -0,0 +1,88 @@ +package gexec + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +/* +The Exit matcher operates on a session: + + Ω(session).Should(Exit()) + +Exit passes if the session has already exited. + +If no status code is provided, then Exit will succeed if the session has exited regardless of exit code. +Otherwise, Exit will only succeed if the process has exited with the provided status code. + +Note that the process must have already exited. To wait for a process to exit, use Eventually: + + Eventually(session, 3).Should(Exit(0)) +*/ +func Exit(optionalExitCode ...int) *exitMatcher { + exitCode := -1 + if len(optionalExitCode) > 0 { + exitCode = optionalExitCode[0] + } + + return &exitMatcher{ + exitCode: exitCode, + } +} + +type exitMatcher struct { + exitCode int + didExit bool + actualExitCode int +} + +type Exiter interface { + ExitCode() int +} + +func (m *exitMatcher) Match(actual interface{}) (success bool, err error) { + exiter, ok := actual.(Exiter) + if !ok { + return false, fmt.Errorf("Exit must be passed a gexec.Exiter (Missing method ExitCode() int) Got:\n%s", format.Object(actual, 1)) + } + + m.actualExitCode = exiter.ExitCode() + + if m.actualExitCode == -1 { + return false, nil + } + + if m.exitCode == -1 { + return true, nil + } + return m.exitCode == m.actualExitCode, nil +} + +func (m *exitMatcher) FailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "Expected process to exit. It did not." + } else { + return format.Message(m.actualExitCode, "to match exit code:", m.exitCode) + } +} + +func (m *exitMatcher) NegatedFailureMessage(actual interface{}) (message string) { + if m.actualExitCode == -1 { + return "you really shouldn't be able to see this!" + } else { + if m.exitCode == -1 { + return "Expected process not to exit. It did." + } else { + return format.Message(m.actualExitCode, "not to match exit code:", m.exitCode) + } + } +} + +func (m *exitMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { + session, ok := actual.(*Session) + if ok { + return session.ExitCode() == -1 + } + return true +} diff --git a/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go b/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go new file mode 100644 index 00000000..05e695ab --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/prefixed_writer.go @@ -0,0 +1,53 @@ +package gexec + +import ( + "io" + "sync" +) + +/* +PrefixedWriter wraps an io.Writer, emiting the passed in prefix at the beginning of each new line. +This can be useful when running multiple gexec.Sessions concurrently - you can prefix the log output of each +session by passing in a PrefixedWriter: + +gexec.Start(cmd, NewPrefixedWriter("[my-cmd] ", GinkgoWriter), NewPrefixedWriter("[my-cmd] ", GinkgoWriter)) +*/ +type PrefixedWriter struct { + prefix []byte + writer io.Writer + lock *sync.Mutex + atStartOfLine bool +} + +func NewPrefixedWriter(prefix string, writer io.Writer) *PrefixedWriter { + return &PrefixedWriter{ + prefix: []byte(prefix), + writer: writer, + lock: &sync.Mutex{}, + atStartOfLine: true, + } +} + +func (w *PrefixedWriter) Write(b []byte) (int, error) { + w.lock.Lock() + defer w.lock.Unlock() + + toWrite := []byte{} + + for _, c := range b { + if w.atStartOfLine { + toWrite = append(toWrite, w.prefix...) + } + + toWrite = append(toWrite, c) + + w.atStartOfLine = c == '\n' + } + + _, err := w.writer.Write(toWrite) + if err != nil { + return 0, err + } + + return len(b), nil +} diff --git a/vendor/github.com/onsi/gomega/gexec/session.go b/vendor/github.com/onsi/gomega/gexec/session.go new file mode 100644 index 00000000..46e71223 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gexec/session.go @@ -0,0 +1,214 @@ +/* +Package gexec provides support for testing external processes. +*/ +package gexec + +import ( + "io" + "os" + "os/exec" + "reflect" + "sync" + "syscall" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +const INVALID_EXIT_CODE = 254 + +type Session struct { + //The wrapped command + Command *exec.Cmd + + //A *gbytes.Buffer connected to the command's stdout + Out *gbytes.Buffer + + //A *gbytes.Buffer connected to the command's stderr + Err *gbytes.Buffer + + //A channel that will close when the command exits + Exited <-chan struct{} + + lock *sync.Mutex + exitCode int +} + +/* +Start starts the passed-in *exec.Cmd command. It wraps the command in a *gexec.Session. + +The session pipes the command's stdout and stderr to two *gbytes.Buffers available as properties on the session: session.Out and session.Err. +These buffers can be used with the gbytes.Say matcher to match against unread output: + + Ω(session.Out).Should(gbytes.Say("foo-out")) + Ω(session.Err).Should(gbytes.Say("foo-err")) + +In addition, Session satisfies the gbytes.BufferProvider interface and provides the stdout *gbytes.Buffer. This allows you to replace the first line, above, with: + + Ω(session).Should(gbytes.Say("foo-out")) + +When outWriter and/or errWriter are non-nil, the session will pipe stdout and/or stderr output both into the session *gybtes.Buffers and to the passed-in outWriter/errWriter. +This is useful for capturing the process's output or logging it to screen. In particular, when using Ginkgo it can be convenient to direct output to the GinkgoWriter: + + session, err := Start(command, GinkgoWriter, GinkgoWriter) + +This will log output when running tests in verbose mode, but - otherwise - will only log output when a test fails. + +The session wrapper is responsible for waiting on the *exec.Cmd command. You *should not* call command.Wait() yourself. +Instead, to assert that the command has exited you can use the gexec.Exit matcher: + + Ω(session).Should(gexec.Exit()) + +When the session exits it closes the stdout and stderr gbytes buffers. This will short circuit any +Eventuallys waiting fo the buffers to Say something. +*/ +func Start(command *exec.Cmd, outWriter io.Writer, errWriter io.Writer) (*Session, error) { + exited := make(chan struct{}) + + session := &Session{ + Command: command, + Out: gbytes.NewBuffer(), + Err: gbytes.NewBuffer(), + Exited: exited, + lock: &sync.Mutex{}, + exitCode: -1, + } + + var commandOut, commandErr io.Writer + + commandOut, commandErr = session.Out, session.Err + + if outWriter != nil && !reflect.ValueOf(outWriter).IsNil() { + commandOut = io.MultiWriter(commandOut, outWriter) + } + + if errWriter != nil && !reflect.ValueOf(errWriter).IsNil() { + commandErr = io.MultiWriter(commandErr, errWriter) + } + + command.Stdout = commandOut + command.Stderr = commandErr + + err := command.Start() + if err == nil { + go session.monitorForExit(exited) + } + + return session, err +} + +/* +Buffer implements the gbytes.BufferProvider interface and returns s.Out +This allows you to make gbytes.Say matcher assertions against stdout without having to reference .Out: + + Eventually(session).Should(gbytes.Say("foo")) +*/ +func (s *Session) Buffer() *gbytes.Buffer { + return s.Out +} + +/* +ExitCode returns the wrapped command's exit code. If the command hasn't exited yet, ExitCode returns -1. + +To assert that the command has exited it is more convenient to use the Exit matcher: + + Eventually(s).Should(gexec.Exit()) + +When the process exits because it has received a particular signal, the exit code will be 128+signal-value +(See http://www.tldp.org/LDP/abs/html/exitcodes.html and http://man7.org/linux/man-pages/man7/signal.7.html) + +*/ +func (s *Session) ExitCode() int { + s.lock.Lock() + defer s.lock.Unlock() + return s.exitCode +} + +/* +Wait waits until the wrapped command exits. It can be passed an optional timeout. +If the command does not exit within the timeout, Wait will trigger a test failure. + +Wait returns the session, making it possible to chain: + + session.Wait().Out.Contents() + +will wait for the command to exit then return the entirety of Out's contents. + +Wait uses eventually under the hood and accepts the same timeout/polling intervals that eventually does. +*/ +func (s *Session) Wait(timeout ...interface{}) *Session { + EventuallyWithOffset(1, s, timeout...).Should(Exit()) + return s +} + +/* +Kill sends the running command a SIGKILL signal. It does not wait for the process to exit. + +If the command has already exited, Kill returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Kill() *Session { + if s.ExitCode() != -1 { + return s + } + s.Command.Process.Kill() + return s +} + +/* +Interrupt sends the running command a SIGINT signal. It does not wait for the process to exit. + +If the command has already exited, Interrupt returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Interrupt() *Session { + return s.Signal(syscall.SIGINT) +} + +/* +Terminate sends the running command a SIGTERM signal. It does not wait for the process to exit. + +If the command has already exited, Terminate returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Terminate() *Session { + return s.Signal(syscall.SIGTERM) +} + +/* +Terminate sends the running command the passed in signal. It does not wait for the process to exit. + +If the command has already exited, Signal returns silently. + +The session is returned to enable chaining. +*/ +func (s *Session) Signal(signal os.Signal) *Session { + if s.ExitCode() != -1 { + return s + } + s.Command.Process.Signal(signal) + return s +} + +func (s *Session) monitorForExit(exited chan<- struct{}) { + err := s.Command.Wait() + s.lock.Lock() + s.Out.Close() + s.Err.Close() + status := s.Command.ProcessState.Sys().(syscall.WaitStatus) + if status.Signaled() { + s.exitCode = 128 + int(status.Signal()) + } else { + exitStatus := status.ExitStatus() + if exitStatus == -1 && err != nil { + s.exitCode = INVALID_EXIT_CODE + } + s.exitCode = exitStatus + } + s.lock.Unlock() + + close(exited) +}