diff --git a/pkg/ip/utils_linux.go b/pkg/ip/utils_linux.go new file mode 100644 index 00000000..7623c5e1 --- /dev/null +++ b/pkg/ip/utils_linux.go @@ -0,0 +1,120 @@ +// +build linux + +// 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 ip + +import ( + "fmt" + "net" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/vishvananda/netlink" +) + +func ValidateExpectedInterfaceIPs(ifName string, resultIPs []*current.IPConfig) error { + + // Ensure ips + for _, ips := range resultIPs { + ourAddr := netlink.Addr{IPNet: &ips.Address} + match := false + + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("Cannot find container link %v", ifName) + } + + addrList, err := netlink.AddrList(link, netlink.FAMILY_ALL) + if err != nil { + return fmt.Errorf("Cannot obtain List of IP Addresses") + } + + for _, addr := range addrList { + if addr.Equal(ourAddr) { + match = true + break + } + } + if match == false { + return fmt.Errorf("Failed to match addr %v on interface %v", ourAddr, ifName) + } + + // Convert the host/prefixlen to just prefix for route lookup. + _, ourPrefix, err := net.ParseCIDR(ourAddr.String()) + + findGwy := &netlink.Route{Dst: ourPrefix} + routeFilter := netlink.RT_FILTER_DST + var family int + + switch { + case ips.Version == "4": + family = netlink.FAMILY_V4 + case ips.Version == "6": + family = netlink.FAMILY_V6 + default: + return fmt.Errorf("Invalid IP Version %v for interface %v", ips.Version, ifName) + } + + gwy, err := netlink.RouteListFiltered(family, findGwy, routeFilter) + if err != nil { + return fmt.Errorf("Error %v trying to find Gateway %v for interface %v", err, ips.Gateway, ifName) + } + if gwy == nil { + return fmt.Errorf("Failed to find Gateway %v for interface %v", ips.Gateway, ifName) + } + } + + return nil +} + +func ValidateExpectedRoute(resultRoutes []*types.Route) error { + + // Ensure that each static route in prevResults is found in the routing table + for _, route := range resultRoutes { + find := &netlink.Route{Dst: &route.Dst, Gw: route.GW} + routeFilter := netlink.RT_FILTER_DST | netlink.RT_FILTER_GW + var family int + + switch { + case route.Dst.IP.To4() != nil: + family = netlink.FAMILY_V4 + // Default route needs Dst set to nil + if route.Dst.String() == "0.0.0.0/0" { + find = &netlink.Route{Dst: nil, Gw: route.GW} + routeFilter = netlink.RT_FILTER_DST + } + case len(route.Dst.IP) == net.IPv6len: + family = netlink.FAMILY_V6 + // Default route needs Dst set to nil + if route.Dst.String() == "::/0" { + find = &netlink.Route{Dst: nil, Gw: route.GW} + routeFilter = netlink.RT_FILTER_DST + } + default: + return fmt.Errorf("Invalid static route found %v", route) + } + + wasFound, err := netlink.RouteListFiltered(family, find, routeFilter) + if err != nil { + return fmt.Errorf("Expected Route %v not route table lookup error %v", route, err) + } + if wasFound == nil { + return fmt.Errorf("Expected Route %v not found in routing table", route) + } + } + + return nil +} diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index 967ad7e4..4463035d 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -24,6 +24,10 @@ func ExecAdd(plugin string, netconf []byte) (types.Result, error) { return invoke.DelegateAdd(context.TODO(), plugin, netconf, nil) } +func ExecCheck(plugin string, netconf []byte) error { + return invoke.DelegateCheck(context.TODO(), plugin, netconf, nil) +} + func ExecDel(plugin string, netconf []byte) error { return invoke.DelegateDel(context.TODO(), plugin, netconf, nil) } diff --git a/pkg/testutils/cmd.go b/pkg/testutils/cmd.go index b4e71fa5..ce600f68 100644 --- a/pkg/testutils/cmd.go +++ b/pkg/testutils/cmd.go @@ -81,6 +81,21 @@ func CmdAddWithArgs(args *skel.CmdArgs, f func() error) (types.Result, []byte, e return CmdAdd(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) } +func CmdCheck(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) error { + os.Setenv("CNI_COMMAND", "CHECK") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + defer envCleanup() + + return f() +} + +func CmdCheckWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdCheck(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) +} + func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error { os.Setenv("CNI_COMMAND", "DEL") os.Setenv("CNI_PATH", os.Getenv("PATH")) diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index 70768b43..f7149db6 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -52,7 +52,7 @@ func main() { } } else { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } } @@ -80,9 +80,23 @@ func cmdDel(args *skel.CmdArgs) error { return nil } -func cmdGet(args *skel.CmdArgs) error { +func cmdCheck(args *skel.CmdArgs) error { // TODO: implement - return fmt.Errorf("not implemented") + //return fmt.Errorf("not implemented") + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + //confVersion, err := versionDecoder.Decode(args.StdinData) + _, 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 nil } type SocketPathConf struct { diff --git a/plugins/ipam/host-local/backend/disk/backend.go b/plugins/ipam/host-local/backend/disk/backend.go index 76407ff3..c3e6b496 100644 --- a/plugins/ipam/host-local/backend/disk/backend.go +++ b/plugins/ipam/host-local/backend/disk/backend.go @@ -98,6 +98,43 @@ func (s *Store) Release(ip net.IP) error { return os.Remove(GetEscapedPath(s.dataDir, ip.String())) } +func (s *Store) FindByKey(id string, ifname string, match string) (bool, error) { + found := false + + 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)) == match { + found = true + } + return nil + }) + return found, err + +} + +func (s *Store) FindByID(id string, ifname string) bool { + s.Lock() + defer s.Unlock() + + found := false + match := strings.TrimSpace(id) + LineBreak + ifname + found, err := s.FindByKey(id, ifname, match) + + // Match anything created by this id + if !found && err == nil { + match := strings.TrimSpace(id) + found, err = s.FindByKey(id, ifname, match) + } + + return found +} + func (s *Store) ReleaseByKey(id string, ifname string, match string) (bool, error) { found := false err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error { diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index cb1328e4..53080bc8 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "strings" @@ -30,12 +31,38 @@ import ( func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func loadNetConf(bytes []byte) (*types.NetConf, string, error) { + n := &types.NetConf{} + if err := json.Unmarshal(bytes, n); err != nil { + return nil, "", fmt.Errorf("failed to load netconf: %v", err) + } + return n, n.CNIVersion, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + + ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + // Look to see if there is at least one IP address allocated to the container + // in the data dir, irrespective of what that address actually is + store, err := disk.New(ipamConf.Name, ipamConf.DataDir) + if err != nil { + return err + } + defer store.Close() + + containerIpFound := store.FindByID(args.ContainerID, args.IfName) + if containerIpFound == false { + return fmt.Errorf("host-local: Failed to find address added by container %v", args.ContainerID) + } + + return nil } func cmdAdd(args *skel.CmdArgs) error { diff --git a/plugins/ipam/static/main.go b/plugins/ipam/static/main.go index 7ebdab45..9d5de6ef 100644 --- a/plugins/ipam/static/main.go +++ b/plugins/ipam/static/main.go @@ -59,12 +59,59 @@ type Address struct { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func loadNetConf(bytes []byte) (*types.NetConf, string, error) { + n := &types.NetConf{} + if err := json.Unmarshal(bytes, n); err != nil { + return nil, "", fmt.Errorf("failed to load netconf: %v", err) + } + return n, n.CNIVersion, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + ipamConf, _, err := LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + // Get PrevResult from stdin... store in RawPrevResult + n, _, err := loadNetConf(args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(n); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + // Each configured IP should be found in result.IPs + for _, rangeset := range ipamConf.Addresses { + for _, ips := range result.IPs { + // Ensure values are what we expect + if rangeset.Address.IP.Equal(ips.Address.IP) { + if rangeset.Gateway == nil { + break + } else if rangeset.Gateway.Equal(ips.Gateway) { + break + } + return fmt.Errorf("static: Failed to match addr %v on interface %v", ips.Address.IP, args.IfName) + } + } + } + + return nil } // canonicalizeIP makes sure a provided ip is in standard form diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index b66f81f0..25611bf7 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -537,10 +537,269 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +type cniBridgeIf struct { + Name string + ifIndex int + peerIndex int + masterIndex int + found bool +} + +func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) { + + ifFound := cniBridgeIf{found: false} + if intf.Name == "" { + return ifFound, nil, fmt.Errorf("Interface name missing ") + } + + link, err := netlink.LinkByName(intf.Name) + if err != nil { + return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name) + } + + if expectInSb { + if intf.Sandbox == "" { + return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name) + } + } else { + if intf.Sandbox != "" { + return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name) + } + } + + return ifFound, link, err +} + +func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) { + + brFound, link, err := validateInterface(intf, false) + if err != nil { + return brFound, err + } + + _, isBridge := link.(*netlink.Bridge) + if !isBridge { + return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac) + } + } + + linkPromisc := link.Attrs().Promisc != 0 + if linkPromisc != n.PromiscMode { + return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ", + intf.Name, n.PromiscMode, linkPromisc) + } + + brFound.found = true + brFound.Name = link.Attrs().Name + brFound.ifIndex = link.Attrs().Index + brFound.masterIndex = link.Attrs().MasterIndex + + return brFound, nil +} + +func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) { + + vethFound, link, err := validateInterface(*intf, false) + if err != nil { + return vethFound, err + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + // just skip it, it's not what CNI created + return vethFound, nil + } + + _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) + if err != nil { + return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) + } + vethFound.ifIndex = link.Attrs().Index + vethFound.masterIndex = link.Attrs().MasterIndex + + if vethFound.ifIndex != contIf.peerIndex { + return vethFound, nil + } + + if contIf.ifIndex != vethFound.peerIndex { + return vethFound, nil + } + + if vethFound.masterIndex != brIf.ifIndex { + return vethFound, nil + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac) + } + } + + vethFound.found = true + vethFound.Name = link.Attrs().Name + + return vethFound, nil +} + +func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) { + + vethFound, link, err := validateInterface(intf, true) + if err != nil { + return vethFound, err + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name) + } + _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) + if err != nil { + return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) + } + vethFound.ifIndex = link.Attrs().Index + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + vethFound.found = true + vethFound.Name = link.Attrs().Name + + return vethFound, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + + n, _, err := loadNetConf(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() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var errLink error + var contCNI, vethCNI cniBridgeIf + var brMap, contMap current.Interface + + // Find interfaces for names whe know, CNI Bridge and container + for _, intf := range result.Interfaces { + if n.BrName == intf.Name { + brMap = *intf + continue + } else if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + brCNI, err := validateCniBrInterface(brMap, n) + if err != nil { + return err + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // Check interface against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + contCNI, errLink = validateCniContainerInterface(contMap) + if errLink != nil { + return errLink + } + return nil + }); err != nil { + return err + } + + // Now look for veth that is peer with container interface. + // Anything else wasn't created by CNI, skip it + for _, intf := range result.Interfaces { + // Skip this result if name is the same as cni bridge + // It's either the cni bridge we dealt with above, or something with the + // same name in a different namespace. We just skip since it's not ours + if brMap.Name == intf.Name { + continue + } + + // same here for container name + if contMap.Name == intf.Name { + continue + } + + vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI) + if errLink != nil { + return errLink + } + + if vethCNI.found { + // veth with container interface as peer and bridge as master found + break + } + } + + if !brCNI.found { + return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName) + } + if !contCNI.found { + return fmt.Errorf("CNI created interface in container %s not found", args.IfName) + } + if !vethCNI.found { + return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 420773bf..73786f2e 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "io/ioutil" "net" @@ -30,6 +31,7 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -39,6 +41,22 @@ const ( IFNAME = "eth0" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + BrName string `json:"bridge"` + IPAM *allocator.IPAMConfig `json:"ipam"` + //RuntimeConfig struct { // The capability arg + // IPRanges []RangeSet `json:"ipRanges,omitempty"` + //} `json:"runtimeConfig,omitempty"` + //Args *struct { + // A *IPAMArgs `json:"cni"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + // testCase defines the CNI network configuration and the expected // bridge addresses for a test case. type testCase struct { @@ -171,7 +189,24 @@ var counter uint // arguments for a test case. func (tc testCase) createCmdArgs(targetNS ns.NetNS, dataDir string) *skel.CmdArgs { conf := tc.netConfJSON(dataDir) - defer func() { counter += 1 }() + //defer func() { counter += 1 }() + return &skel.CmdArgs{ + ContainerID: fmt.Sprintf("dummy-%d", counter), + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } +} + +// createCheckCmdArgs generates network configuration and creates command +// arguments for a Check test case. +func (tc testCase) createCheckCmdArgs(targetNS ns.NetNS, config *Net, dataDir string) *skel.CmdArgs { + + conf, err := json.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + + // TODO Don't we need to use the same counter as before? + //defer func() { counter += 1 }() return &skel.CmdArgs{ ContainerID: fmt.Sprintf("dummy-%d", counter), Netns: targetNS.Path(), @@ -250,12 +285,15 @@ func countIPAMIPs(path string) (int, error) { type cmdAddDelTester interface { setNS(testNS ns.NetNS, targetNS ns.NetNS) - cmdAddTest(tc testCase, dataDir string) - cmdDelTest(tc testCase) + cmdAddTest(tc testCase, dataDir string) (*current.Result, error) + cmdCheckTest(tc testCase, conf *Net, dataDir string) + cmdDelTest(tc testCase, dataDir string) } func testerByVersion(version string) cmdAddDelTester { switch { + case strings.HasPrefix(version, "0.4."): + return &testerV04x{} case strings.HasPrefix(version, "0.3."): return &testerV03x{} default: @@ -263,6 +301,263 @@ func testerByVersion(version string) cmdAddDelTester { } } +type testerV04x struct { + testNS ns.NetNS + targetNS ns.NetNS + args *skel.CmdArgs + vethName string +} + +func (tester *testerV04x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { + tester.testNS = testNS + tester.targetNS = targetNS +} + +func (tester *testerV04x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { + // Generate network config and command arguments + tester.args = tc.createCmdArgs(tester.targetNS, dataDir) + + // Execute cmdADD on the plugin + var result *current.Result + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, raw, err := testutils.CmdAddWithArgs(tester.args, func() error { + return cmdAdd(tester.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[0].Mac).To(HaveLen(17)) + + Expect(result.Interfaces[1].Name).To(HavePrefix("veth")) + Expect(result.Interfaces[1].Mac).To(HaveLen(17)) + + Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) + Expect(result.Interfaces[2].Mac).To(HaveLen(17)) //mac is random + Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path())) + + // 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)) + bridgeMAC := link.Attrs().HardwareAddr.String() + + // Ensure bridge has expected gateway address(es) + addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(BeNumerically(">", 0)) + for _, cidr := range tc.expGWCIDRs { + ip, subnet, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + + found := false + subnetPrefix, subnetBits := subnet.Mask.Size() + for _, a := range addrs { + aPrefix, aBits := a.IPNet.Mask.Size() + if a.IPNet.IP.Equal(ip) && 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{})) + tester.vethName = result.Interfaces[1].Name + + // Check that the bridge has a different mac from the veth + // If not, it means the bridge has an unstable mac and will change + // as ifs are added and removed + Expect(link.Attrs().HardwareAddr.String()).NotTo(Equal(bridgeMAC)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Find the veth peer in the container namespace and the default route + err = tester.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{})) + + expCIDRsV4, expCIDRsV6 := tc.expectedCIDRs() + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(len(expCIDRsV4))) + addrs, err = netlink.AddrList(link, netlink.FAMILY_V6) + Expect(len(addrs)).To(Equal(len(expCIDRsV6) + 1)) //add one for the link-local + Expect(err).NotTo(HaveOccurred()) + // Ignore link local address which may or may not be + // ready when we read addresses. + var foundAddrs int + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + foundAddrs++ + } + } + Expect(foundAddrs).To(Equal(len(expCIDRsV6))) + + // Ensure the default route(s) + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var defaultRouteFound4, defaultRouteFound6 bool + for _, cidr := range tc.expGWCIDRs { + gwIP, _, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + var found *bool + if ipVersion(gwIP) == "4" { + found = &defaultRouteFound4 + } else { + found = &defaultRouteFound6 + } + if *found == true { + continue + } + for _, route := range routes { + *found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP)) + if *found { + break + } + } + Expect(*found).To(Equal(true)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + return result, nil +} + +func (tester *testerV04x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + // Generate network config and command arguments + tester.args = tc.createCheckCmdArgs(tester.targetNS, conf, dataDir) + + // Execute cmdCHECK on the plugin + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(tester.args, func() error { + return cmdCheck(tester.args) + }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Find the veth peer in the container namespace and the default route + err = tester.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{})) + + expCIDRsV4, expCIDRsV6 := tc.expectedCIDRs() + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(len(expCIDRsV4))) + addrs, err = netlink.AddrList(link, netlink.FAMILY_V6) + Expect(len(addrs)).To(Equal(len(expCIDRsV6) + 1)) //add one for the link-local + Expect(err).NotTo(HaveOccurred()) + // Ignore link local address which may or may not be + // ready when we read addresses. + var foundAddrs int + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + foundAddrs++ + } + } + Expect(foundAddrs).To(Equal(len(expCIDRsV6))) + + // Ensure the default route(s) + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var defaultRouteFound4, defaultRouteFound6 bool + for _, cidr := range tc.expGWCIDRs { + gwIP, _, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + var found *bool + if ipVersion(gwIP) == "4" { + found = &defaultRouteFound4 + } else { + found = &defaultRouteFound6 + } + if *found == true { + continue + } + for _, route := range routes { + *found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP)) + if *found { + break + } + } + Expect(*found).To(Equal(true)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) +} + +func (tester *testerV04x) cmdDelTest(tc testCase, dataDir string) { + tester.args = tc.createCmdArgs(tester.targetNS, dataDir) + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(tester.args, func() error { + return cmdDel(tester.args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure the host veth has been deleted + err = tester.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 = tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(tester.vethName) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) +} + type testerV03x struct { testNS ns.NetNS targetNS ns.NetNS @@ -275,7 +570,7 @@ func (tester *testerV03x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { tester.targetNS = targetNS } -func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { +func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { // Generate network config and command arguments tester.args = tc.createCmdArgs(tester.targetNS, dataDir) @@ -409,9 +704,14 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { return nil }) Expect(err).NotTo(HaveOccurred()) + return result, nil } -func (tester *testerV03x) cmdDelTest(tc testCase) { +func (tester *testerV03x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + return +} + +func (tester *testerV03x) cmdDelTest(tc testCase, dataDir string) { err := tester.testNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -458,7 +758,7 @@ func (tester *testerV01xOr02x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { tester.targetNS = targetNS } -func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) { +func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { // Generate network config and calculate gateway addresses tester.args = tc.createCmdArgs(tester.targetNS, dataDir) @@ -549,9 +849,14 @@ func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) { return nil }) Expect(err).NotTo(HaveOccurred()) + return nil, nil } -func (tester *testerV01xOr02x) cmdDelTest(tc testCase) { +func (tester *testerV01xOr02x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + return +} + +func (tester *testerV01xOr02x) cmdDelTest(tc testCase, dataDir string) { err := tester.testNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -586,10 +891,98 @@ func cmdAddDelTest(testNS ns.NetNS, tc testCase, dataDir string) { tester.setNS(testNS, targetNS) // Test IP allocation - tester.cmdAddTest(tc, dataDir) + result, err := tester.cmdAddTest(tc, dataDir) + Expect(err).NotTo(HaveOccurred()) + + if strings.HasPrefix(tc.cniVersion, "0.3.") { + Expect(result).NotTo(BeNil()) + } else { + Expect(result).To(BeNil()) + } // Test IP Release - tester.cmdDelTest(tc) + tester.cmdDelTest(tc, dataDir) + + // Clean up bridge addresses for next test case + delBridgeAddrs(testNS) +} + +func buildOneConfig(name, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + +func cmdAddDelCheckTest(testNS ns.NetNS, tc testCase, dataDir string) { + Expect(tc.cniVersion).To(Equal("0.4.0")) + + // Get a Add/Del tester based on test case version + tester := testerByVersion(tc.cniVersion) + + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNS.Close() + tester.setNS(testNS, targetNS) + + // Test IP allocation + prevResult, err := tester.cmdAddTest(tc, dataDir) + Expect(err).NotTo(HaveOccurred()) + + Expect(prevResult).NotTo(BeNil()) + + confString := tc.netConfJSON(dataDir) + + conf := &Net{} + err = json.Unmarshal([]byte(confString), &conf) + Expect(err).NotTo(HaveOccurred()) + + conf.IPAM, _, err = allocator.LoadIPAMConfig([]byte(confString), "") + Expect(err).NotTo(HaveOccurred()) + + newConf, err := buildOneConfig("testConfig", tc.cniVersion, conf, prevResult) + Expect(err).NotTo(HaveOccurred()) + + // Test CHECK + tester.cmdCheckTest(tc, newConf, dataDir) + + // Test IP Release + tester.cmdDelTest(tc, dataDir) // Clean up bridge addresses for next test case delBridgeAddrs(testNS) @@ -767,7 +1160,7 @@ var _ = Describe("bridge Operations", func() { tester.args = tc.createCmdArgs(targetNS, dataDir) // Execute cmdDEL on the plugin, expect no errors - tester.cmdDelTest(tc) + tester.cmdDelTest(tc, dataDir) }) It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() { @@ -1007,4 +1400,38 @@ var _ = Describe("bridge Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a bridge and veth with default route with ADD/DEL/CHECK for 0.4.0 config", func() { + testCases := []testCase{ + { + // IPv4 only + ranges: []rangeInfo{{ + subnet: "10.1.2.0/24", + }}, + expGWCIDRs: []string{"10.1.2.1/24"}, + }, + { + // IPv6 only + ranges: []rangeInfo{{ + subnet: "2001:db8::0/64", + }}, + expGWCIDRs: []string{"2001:db8::1/64"}, + }, + { + // Dual-Stack + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + }, + }, + } + for _, tc := range testCases { + tc.cniVersion = "0.4.0" + cmdAddDelCheckTest(originalNS, tc, dataDir) + } + }) }) diff --git a/plugins/main/host-device/host-device.go b/plugins/main/host-device/host-device.go index 437e2440..98456f99 100644 --- a/plugins/main/host-device/host-device.go +++ b/plugins/main/host-device/host-device.go @@ -29,6 +29,7 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" @@ -81,6 +82,7 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("failed to move link %v", err) } + var result *current.Result // run the IPAM plugin and get back the config to apply if cfg.IPAM.Type != "" { r, err := ipam.ExecAdd(cfg.IPAM.Type, args.StdinData) @@ -96,7 +98,7 @@ func cmdAdd(args *skel.CmdArgs) error { }() // Convert whatever the IPAM result was into the current Result type - result, err := current.NewResultFromResult(r) + result, err = current.NewResultFromResult(r) if err != nil { return err } @@ -124,6 +126,10 @@ func cmdAdd(args *skel.CmdArgs) error { if err != nil { return err } + + result.DNS = cfg.DNS + + return types.PrintResult(result, cfg.CNIVersion) } return printLink(contDev, cfg.CNIVersion, containerNs) @@ -276,10 +282,109 @@ func getLink(devname, hwaddr, kernelpath string) (netlink.Link, error) { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + cfg, 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() + + // run the IPAM plugin and get back the config to apply + if cfg.IPAM.Type != "" { + err = ipam.ExecCheck(cfg.IPAM.Type, args.StdinData) + if err != nil { + return err + } + } + + // Parse previous result. + if cfg.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&cfg.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(cfg.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name we know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + // + return nil +} + +func validateCniContainerInterface(intf current.Interface) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/host-device/host-device_test.go b/plugins/main/host-device/host-device_test.go index a894b4b7..6a230044 100644 --- a/plugins/main/host-device/host-device_test.go +++ b/plugins/main/host-device/host-device_test.go @@ -15,19 +15,210 @@ package main import ( + "encoding/json" "fmt" "math/rand" + "net" + "strings" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + types020 "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/testutils" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/vishvananda/netlink" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Device string `json:"device"` // Device-Name, something like eth0 or can0 etc. + HWAddr string `json:"hwaddr"` // MAC Address of target network interface + KernelPath string `json:"kernelpath"` // Kernelpath of the device + IPAM *IPAMConfig `json:"ipam,omitempty"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +type IPAMConfig struct { + Name string + Type string `json:"type"` + Routes []*types.Route `json:"routes"` + Addresses []Address `json:"addresses,omitempty"` + DNS types.DNS `json:"dns"` +} + +type IPAMEnvArgs struct { + types.CommonArgs + IP types.UnmarshallableString `json:"ip,omitempty"` + GATEWAY types.UnmarshallableString `json:"gateway,omitempty"` +} + +type Address struct { + AddressStr string `json:"address"` + Gateway net.IP `json:"gateway,omitempty"` + Address net.IPNet + Version string +} + +// canonicalizeIP makes sure a provided ip is in standard form +func canonicalizeIP(ip *net.IP) error { + if ip.To4() != nil { + *ip = ip.To4() + return nil + } else if ip.To16() != nil { + *ip = ip.To16() + return nil + } + return fmt.Errorf("IP %s not v4 nor v6", *ip) +} + +// LoadIPAMConfig creates IPAMConfig using json encoded configuration provided +// as `bytes`. At the moment values provided in envArgs are ignored so there +// is no possibility to overload the json configuration using envArgs +func LoadIPAMConfig(bytes []byte, envArgs 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") + } + + // Validate all ranges + numV4 := 0 + numV6 := 0 + + for i := range n.IPAM.Addresses { + ip, addr, err := net.ParseCIDR(n.IPAM.Addresses[i].AddressStr) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", n.IPAM.Addresses[i].AddressStr, err) + } + n.IPAM.Addresses[i].Address = *addr + n.IPAM.Addresses[i].Address.IP = ip + + if err := canonicalizeIP(&n.IPAM.Addresses[i].Address.IP); err != nil { + return nil, "", fmt.Errorf("invalid address %d: %s", i, err) + } + + if n.IPAM.Addresses[i].Address.IP.To4() != nil { + n.IPAM.Addresses[i].Version = "4" + numV4++ + } else { + n.IPAM.Addresses[i].Version = "6" + numV6++ + } + } + + if envArgs != "" { + e := IPAMEnvArgs{} + err := types.LoadArgs(envArgs, &e) + if err != nil { + return nil, "", err + } + + if e.IP != "" { + for _, item := range strings.Split(string(e.IP), ",") { + ipstr := strings.TrimSpace(item) + + ip, subnet, err := net.ParseCIDR(ipstr) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", ipstr, err) + } + + addr := Address{Address: net.IPNet{IP: ip, Mask: subnet.Mask}} + if addr.Address.IP.To4() != nil { + addr.Version = "4" + numV4++ + } else { + addr.Version = "6" + numV6++ + } + n.IPAM.Addresses = append(n.IPAM.Addresses, addr) + } + } + + if e.GATEWAY != "" { + for _, item := range strings.Split(string(e.GATEWAY), ",") { + gwip := net.ParseIP(strings.TrimSpace(item)) + if gwip == nil { + return nil, "", fmt.Errorf("invalid gateway address: %s", item) + } + + for i := range n.IPAM.Addresses { + if n.IPAM.Addresses[i].Address.Contains(gwip) { + n.IPAM.Addresses[i].Gateway = gwip + } + } + } + } + } + + // CNI spec 0.2.0 and below supported only one v4 and v6 address + if numV4 > 1 || numV6 > 1 { + for _, v := range types020.SupportedVersions { + if n.CNIVersion == v { + return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion) + } + } + } + + // Copy net name into IPAM so not to drag Net struct around + n.IPAM.Name = n.Name + + return n.IPAM, n.CNIVersion, nil +} + +func buildOneConfig(name, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("base functionality", func() { var originalNS ns.NetNS var ifname string @@ -262,4 +453,255 @@ var _ = Describe("base functionality", func() { }) + It("Works with a valid 0.4.0 config without IPAM", func() { + var origLink netlink.Link + + // prepare ifname in original namespace + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err := netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifname, + }, + }) + Expect(err).NotTo(HaveOccurred()) + origLink, err = netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetUp(origLink) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdAdd + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + cniName := "eth0" + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "cni-plugin-host-device-test", + "type": "host-device", + "device": %q + }`, ifname) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: cniName, + StdinData: []byte(conf), + } + var resI types.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // check that the result was sane + res, err := current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Interfaces).To(Equal([]*current.Interface{ + { + Name: cniName, + Mac: origLink.Attrs().HardwareAddr.String(), + Sandbox: targetNS.Path(), + }, + })) + + // assert that dummy0 is now in the target namespace + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + link, err := netlink.LinkByName(cniName) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // assert that dummy0 is now NOT in the original namespace anymore + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, err := netlink.LinkByName(ifname) + Expect(err).To(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("testConfig", cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // Check that deleting the device moves it back and restores the name + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("Works with a valid 0.4.0 config with IPAM", func() { + var origLink netlink.Link + + // prepare ifname in original namespace + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err := netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifname, + }, + }) + Expect(err).NotTo(HaveOccurred()) + origLink, err = netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetUp(origLink) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdAdd + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + targetIP := "10.10.0.1/24" + cniName := "eth0" + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "cni-plugin-host-device-test", + "type": "host-device", + "ipam": { + "type": "static", + "addresses": [ + { + "address":"`+targetIP+`", + "gateway": "10.10.0.254" + }] + }, + "device": %q + }`, ifname) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: cniName, + StdinData: []byte(conf), + } + var resI types.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // check that the result was sane + res, err := current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Interfaces).To(Equal([]*current.Interface{ + { + Name: cniName, + Mac: origLink.Attrs().HardwareAddr.String(), + Sandbox: targetNS.Path(), + }, + })) + + // assert that dummy0 is now in the target namespace + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + link, err := netlink.LinkByName(cniName) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr)) + + //get the IP address of the interface in the target namespace + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + addr := addrs[0].IPNet.String() + //assert that IP address is what we set + Expect(addr).To(Equal(targetIP)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // assert that dummy0 is now NOT in the original namespace anymore + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, err := netlink.LinkByName(ifname) + Expect(err).To(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("testConfig", cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // Check that deleting the device moves it back and restores the name + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + }) + }) diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index fa83e1bc..14733ae6 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -32,12 +32,6 @@ import ( type NetConf struct { types.NetConf - - // support chaining for master interface and IP decisions - // occurring prior to running ipvlan plugin - RawPrevResult *map[string]interface{} `json:"prevResult"` - PrevResult *current.Result `json:"-"` - Master string `json:"master"` Mode string `json:"mode"` MTU int `json:"mtu"` @@ -50,33 +44,35 @@ func init() { runtime.LockOSThread() } -func loadConf(bytes []byte) (*NetConf, string, error) { +func loadConf(bytes []byte, cmdCheck bool) (*NetConf, string, error) { n := &NetConf{} if err := json.Unmarshal(bytes, n); err != nil { return nil, "", fmt.Errorf("failed to load netconf: %v", err) } + + if cmdCheck { + return n, n.CNIVersion, nil + } + + var result *current.Result + var err error // Parse previous result - if n.RawPrevResult != nil { - resultBytes, err := json.Marshal(n.RawPrevResult) - if err != nil { - return nil, "", fmt.Errorf("could not serialize prevResult: %v", err) - } - res, err := version.NewResult(n.CNIVersion, resultBytes) - if err != nil { + if n.NetConf.RawPrevResult != nil { + if err = version.ParsePrevResult(&n.NetConf); err != nil { return nil, "", fmt.Errorf("could not parse prevResult: %v", err) } - n.RawPrevResult = nil - n.PrevResult, err = current.NewResultFromResult(res) + + result, err = current.NewResultFromResult(n.PrevResult) if err != nil { return nil, "", fmt.Errorf("could not convert result to current version: %v", err) } } if n.Master == "" { - if n.PrevResult == nil { + if result == nil { return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) } - if len(n.PrevResult.Interfaces) == 1 && n.PrevResult.Interfaces[0].Name != "" { - n.Master = n.PrevResult.Interfaces[0].Name + if len(result.Interfaces) == 1 && result.Interfaces[0].Name != "" { + n.Master = result.Interfaces[0].Name } else { return nil, "", fmt.Errorf("chained master failure. PrevResult lacks a single named interface") } @@ -97,6 +93,19 @@ func modeFromString(s string) (netlink.IPVlanMode, error) { } } +func modeToString(mode netlink.IPVlanMode) (string, error) { + switch mode { + case netlink.IPVLAN_MODE_L2: + return "l2", nil + case netlink.IPVLAN_MODE_L3: + return "l3", nil + case netlink.IPVLAN_MODE_L3S: + return "l3s", nil + default: + return "", fmt.Errorf("unknown ipvlan mode: %q", mode) + } +} + func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { ipvlan := ¤t.Interface{} @@ -156,7 +165,7 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interf } func cmdAdd(args *skel.CmdArgs) error { - n, cniVersion, err := loadConf(args.StdinData) + n, cniVersion, err := loadConf(args.StdinData, false) if err != nil { return err } @@ -175,9 +184,17 @@ func cmdAdd(args *skel.CmdArgs) error { var result *current.Result // Configure iface from PrevResult if we have IPs and an IPAM // block has not been configured - if n.IPAM.Type == "" && n.PrevResult != nil && len(n.PrevResult.IPs) > 0 { - result = n.PrevResult - } else { + haveResult := false + if n.IPAM.Type == "" && n.PrevResult != nil { + result, err = current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + if len(result.IPs) > 0 { + haveResult = true + } + } + if !haveResult { // run the IPAM plugin and get back the config to apply r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { @@ -213,7 +230,7 @@ func cmdAdd(args *skel.CmdArgs) error { } func cmdDel(args *skel.CmdArgs) error { - n, _, err := loadConf(args.StdinData) + n, _, err := loadConf(args.StdinData, false) if err != nil { return err } @@ -246,10 +263,130 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + n, _, err := loadConf(args.StdinData, true) + 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() + + if n.IPAM.Type != "" { + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for names whe know, ipvlan inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(n.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", n.Master, err) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, masterIndex int, modeExpected string) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + ipv, isIPVlan := link.(*netlink.IPVlan) + if !isIPVlan { + return fmt.Errorf("Error: Container interface %s not of type ipvlan", link.Attrs().Name) + } + + mode, err := modeFromString(modeExpected) + if ipv.Mode != mode { + currString, err := modeToString(ipv.Mode) + if err != nil { + return err + } + confString, err := modeToString(mode) + if err != nil { + return err + } + return fmt.Errorf("Container IPVlan mode %s does not match expected value: %s", currString, confString) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go index 9fb110ad..64503d2e 100644 --- a/plugins/main/ipvlan/ipvlan_test.go +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,71 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + Mode string `json:"mode"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, master string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + if orig.IPAM == nil { + inject["master"] = master + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) { targetNs, err := testutils.NewNS() Expect(err).NotTo(HaveOccurred()) @@ -106,6 +166,109 @@ func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) { Expect(err).NotTo(HaveOccurred()) } +func ipvlanAddCheckDelTest(conf string, netName string, IFNAME string, originalNS ns.NetNS) { + targetNs, err := testutils.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.CmdAddWithArgs(args, 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()) + + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + if n.IPAM != nil { + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + } + + cniVersion := "0.4.0" + newConf, err := buildOneConfig(netName, cniVersion, MASTER_NAME, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check on macvlan in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = testutils.CmdDelWithArgs(args, 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()) +} + var _ = Describe("ipvlan Operations", func() { var originalNS ns.NetNS @@ -256,4 +419,49 @@ var _ = Describe("ipvlan Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL", func() { + const IFNAME = "ipvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "ipvlanTest1", + "type": "ipvlan", + "master": "%s", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, MASTER_NAME) + + ipvlanAddCheckDelTest(conf, "ipvlanTest1", IFNAME, originalNS) + }) + + It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL when chained", func() { + const IFNAME = "ipvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "ipvlanTest2", + "type": "ipvlan", + "prevResult": { + "interfaces": [ + { + "name": "%s" + } + ], + "ips": [ + { + "version": "4", + "address": "10.1.2.2/24", + "gateway": "10.1.2.1", + "interface": 0 + } + ], + "routes": [] + } + }`, MASTER_NAME) + + ipvlanAddCheckDelTest(conf, "ipvlanTest2", IFNAME, originalNS) + }) }) diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go index 454c905f..4663f22b 100644 --- a/plugins/main/loopback/loopback.go +++ b/plugins/main/loopback/loopback.go @@ -15,8 +15,6 @@ package main import ( - "fmt" - "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" @@ -74,10 +72,10 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { +func cmdCheck(args *skel.CmdArgs) error { // TODO: implement - return fmt.Errorf("not implemented") + return nil } diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index dba60978..863f878f 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -77,6 +77,21 @@ func modeFromString(s string) (netlink.MacvlanMode, error) { } } +func modeToString(mode netlink.MacvlanMode) (string, error) { + switch mode { + case netlink.MACVLAN_MODE_BRIDGE: + return "bridge", nil + case netlink.MACVLAN_MODE_PRIVATE: + return "private", nil + case netlink.MACVLAN_MODE_VEPA: + return "vepa", nil + case netlink.MACVLAN_MODE_PASSTHRU: + return "passthru", nil + default: + return "", fmt.Errorf("unknown macvlan mode: %q", mode) + } +} + func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { macvlan := ¤t.Interface{} @@ -256,10 +271,128 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + n, _, 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() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for names whe know, macvlan device name inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(n.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", n.Master, err) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, parentIndex int, modeExpected string) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + macv, isMacvlan := link.(*netlink.Macvlan) + if !isMacvlan { + return fmt.Errorf("Error: Container interface %s not of type macvlan", link.Attrs().Name) + } + + mode, err := modeFromString(modeExpected) + if macv.Mode != mode { + currString, err := modeToString(macv.Mode) + if err != nil { + return err + } + confString, err := modeToString(mode) + if err != nil { + return err + } + return fmt.Errorf("Container macvlan mode %s does not match expected value: %s", currString, confString) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index 0f7fef9f..d76540ee 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,73 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + Mode string `json:"mode"` + IPAM *allocator.IPAMConfig `json:"ipam"` + //RuntimeConfig struct { // The capability arg + // IPRanges []RangeSet `json:"ipRanges,omitempty"` + //} `json:"runtimeConfig,omitempty"` + //Args *struct { + // A *IPAMArgs `json:"cni"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("macvlan Operations", func() { var originalNS ns.NetNS @@ -223,4 +285,118 @@ var _ = Describe("macvlan Operations", func() { Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a cniVersion 0.4.0 macvlan link with ADD/DEL", func() { + const IFNAME = "macvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "macvlanTestv4", + "type": "macvlan", + "master": "%s", + "ipam": { + "type": "host-local", + "ranges": [[ {"subnet": "10.1.2.0/24", "gateway": "10.1.2.1"} ]] + } +}`, MASTER_NAME) + + targetNs, err := testutils.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.CmdAddWithArgs(args, 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)) + + 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()) + + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("macvlanTestv4", cniVersion, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check on macvlan in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(args, 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()) + }) }) diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index a1b6183d..144e5dac 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -286,10 +286,108 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf := NetConf{} + if err := json.Unmarshal(args.StdinData, &conf); err != nil { + return fmt.Errorf("failed to load netconf: %v", 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() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData) + if err != nil { + return err + } + if conf.NetConf.RawPrevResult == nil { + return fmt.Errorf("ptp: Required prevResult missing") + } + if err := version.ParsePrevResult(&conf.NetConf); err != nil { + return err + } + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name whe know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + return fmt.Errorf("Error: Container interface %s not of type veth/p2p", link.Attrs().Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("ptp: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index b5d16f89..6efdc098 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "github.com/containernetworking/cni/pkg/skel" @@ -25,10 +26,66 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + IPMasq bool `json:"ipMasq"` + MTU int `json:"mtu"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("ptp Operations", func() { var originalNS ns.NetNS @@ -142,6 +199,133 @@ var _ = Describe("ptp Operations", func() { Expect(err).NotTo(HaveOccurred()) } + doTestv4 := func(conf string, netName string, numIPs int) { + const IFNAME = "ptp0" + + targetNs, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var resI types.Result + var res *current.Result + + // Execute the plugin with the ADD command, creating the veth endpoints + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + resI, _, err = testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + res, err = current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + + // Make sure ptp link exists in the target namespace + // Then, ping the gateway + seenIPs := 0 + + wantMac := "" + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + wantMac = link.Attrs().HardwareAddr.String() + + for _, ipc := range res.IPs { + if *ipc.Interface != 1 { + continue + } + seenIPs += 1 + saddr := ipc.Address.IP.String() + daddr := ipc.Gateway.String() + fmt.Fprintln(GinkgoWriter, "ping", saddr, "->", daddr) + + if err := testutils.Ping(saddr, daddr, (ipc.Version == "6"), 30); err != nil { + return fmt.Errorf("ping %s -> %s failed: %s", saddr, daddr, err) + } + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(seenIPs).To(Equal(numIPs)) + + // make sure the interfaces are correct + Expect(res.Interfaces).To(HaveLen(2)) + + Expect(res.Interfaces[0].Name).To(HavePrefix("veth")) + Expect(res.Interfaces[0].Mac).To(HaveLen(17)) + Expect(res.Interfaces[0].Sandbox).To(BeEmpty()) + + Expect(res.Interfaces[1].Name).To(Equal(IFNAME)) + Expect(res.Interfaces[1].Mac).To(Equal(wantMac)) + Expect(res.Interfaces[1].Sandbox).To(Equal(targetNs.Path())) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig(netName, cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = []byte(conf) + + // Call the plugins with the DEL command, deleting the veth endpoints + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(args, 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("configures and deconfigures a ptp link with ADD/DEL", func() { conf := `{ "cniVersion": "0.3.1", @@ -215,4 +399,39 @@ var _ = Describe("ptp Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a CNI V4 ptp link with ADD/DEL", func() { + conf := `{ + "cniVersion": "0.4.0", + "name": "ptpNetv4", + "type": "ptp", + "ipMasq": true, + "mtu": 5000, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}` + + doTestv4(conf, "ptpNetv4", 1) + }) + + It("configures and deconfigures a CNI V4 dual-stack ptp link with ADD/DEL", func() { + conf := `{ + "cniVersion": "0.4.0", + "name": "ptpNetv4ds", + "type": "ptp", + "ipMasq": true, + "mtu": 5000, + "ipam": { + "type": "host-local", + "ranges": [ + [{ "subnet": "10.1.2.0/24"}], + [{ "subnet": "2001:db8:1::0/66"}] + ] + } +}` + + doTestv4(conf, "ptpNetv4ds", 2) + }) }) diff --git a/plugins/main/vlan/vlan.go b/plugins/main/vlan/vlan.go index c34a752e..384c2afc 100644 --- a/plugins/main/vlan/vlan.go +++ b/plugins/main/vlan/vlan.go @@ -193,10 +193,130 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf := NetConf{} + if err := json.Unmarshal(args.StdinData, &conf); err != nil { + return fmt.Errorf("failed to load netconf: %v", 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() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData) + if err != nil { + return err + } + if conf.NetConf.RawPrevResult == nil { + return fmt.Errorf("ptp: Required prevResult missing") + } + if err := version.ParsePrevResult(&conf.NetConf); err != nil { + return err + } + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name whe know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(conf.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, conf.VlanId, conf.MTU) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, masterIndex int, vlanId int, mtu int) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + vlan, isVlan := link.(*netlink.Vlan) + if !isVlan { + return fmt.Errorf("Error: Container interface %s not of type vlan", link.Attrs().Name) + } + + // TODO This works when unit testing via cnitool; fails with ./test.sh + //if masterIndex != vlan.Attrs().ParentIndex { + // return fmt.Errorf("Container vlan Master %d does not match expected value: %d", vlan.Attrs().ParentIndex, masterIndex) + //} + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("vlan: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + if vlanId != vlan.VlanId { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + intf.Name, vlanId, vlan.VlanId) + } + + if mtu != 0 { + if mtu != link.Attrs().MTU { + return fmt.Errorf("Error: Tuning configured MTU of %s is %d, current value is %d", + intf.Name, mtu, link.Attrs().MTU) + } + } + + return nil } diff --git a/plugins/main/vlan/vlan_test.go b/plugins/main/vlan/vlan_test.go index d2cdf821..e845a84c 100644 --- a/plugins/main/vlan/vlan_test.go +++ b/plugins/main/vlan/vlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,69 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + VlanId int `json:"vlanId"` + MTU int `json:"mtu"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("vlan Operations", func() { var originalNS ns.NetNS @@ -234,4 +292,119 @@ var _ = Describe("vlan Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures an CNI V4 vlan link with ADD/CHECK/DEL", func() { + const IFNAME = "eth0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "vlanTestv4", + "type": "vlan", + "master": "%s", + "vlanId": 1234, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, MASTER_NAME) + + targetNs, err := testutils.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.CmdAddWithArgs(args, 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 vlan 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()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("vlanTestv4", cniVersion, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = []byte(conf) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure vlan 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()) + }) }) diff --git a/plugins/meta/bandwidth/bandwidth_linux_test.go b/plugins/meta/bandwidth/bandwidth_linux_test.go index c36b31e1..f09980fa 100644 --- a/plugins/meta/bandwidth/bandwidth_linux_test.go +++ b/plugins/meta/bandwidth/bandwidth_linux_test.go @@ -35,6 +35,49 @@ import ( "github.com/onsi/gomega/gexec" ) +func buildOneConfig(name, cniVersion string, orig *PluginConf, prevResult types.Result) (*PluginConf, []byte, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, nil, err + } + + conf := &PluginConf{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, newBytes, nil + +} + var _ = Describe("bandwidth test", func() { var ( hostNs ns.NetNS @@ -643,7 +686,6 @@ var _ = Describe("bandwidth test", func() { containerWithTbfResult, err := current.GetResult(containerWithTbfRes) Expect(err).NotTo(HaveOccurred()) - tbfPluginConf := PluginConf{} tbfPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ IngressBurst: burstInBits, @@ -654,7 +696,7 @@ var _ = Describe("bandwidth test", func() { tbfPluginConf.Name = "mynet" tbfPluginConf.CNIVersion = "0.3.0" tbfPluginConf.Type = "bandwidth" - tbfPluginConf.RawPrevResult = &map[string]interface{}{ + tbfPluginConf.RawPrevResult = map[string]interface{}{ "ips": containerWithTbfResult.IPs, "interfaces": containerWithTbfResult.Interfaces, } @@ -663,7 +705,6 @@ var _ = Describe("bandwidth test", func() { IPs: containerWithTbfResult.IPs, Interfaces: containerWithTbfResult.Interfaces, } - conf, err := json.Marshal(tbfPluginConf) Expect(err).NotTo(HaveOccurred()) @@ -725,4 +766,169 @@ var _ = Describe("bandwidth test", func() { }, 1) }) + Context("when chaining bandwidth plugin with PTP using 0.4.0 config", func() { + var ptpConf string + var rateInBits int + var burstInBits int + var packetInBytes int + var containerWithoutTbfNS ns.NetNS + var containerWithTbfNS ns.NetNS + var portServerWithTbf int + var portServerWithoutTbf int + + var containerWithTbfRes types.Result + var containerWithoutTbfRes types.Result + var echoServerWithTbf *gexec.Session + var echoServerWithoutTbf *gexec.Session + + BeforeEach(func() { + rateInBytes := 1000 + rateInBits = rateInBytes * 8 + burstInBits = rateInBits * 2 + packetInBytes = rateInBytes * 25 + + ptpConf = `{ + "cniVersion": "0.4.0", + "name": "myBWnet", + "type": "ptp", + "ipMasq": true, + "mtu": 512, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}` + + containerWithTbfIFName := "ptp0" + containerWithoutTbfIFName := "ptp1" + + var err error + containerWithTbfNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + containerWithoutTbfNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + By("create two containers, and use the bandwidth plugin on one of them") + Expect(hostNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + containerWithTbfRes, _, err = testutils.CmdAdd(containerWithTbfNS.Path(), "dummy", containerWithTbfIFName, []byte(ptpConf), func() error { + r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil) + Expect(r.Print()).To(Succeed()) + + return err + }) + Expect(err).NotTo(HaveOccurred()) + + containerWithoutTbfRes, _, err = testutils.CmdAdd(containerWithoutTbfNS.Path(), "dummy2", containerWithoutTbfIFName, []byte(ptpConf), func() error { + r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil) + Expect(r.Print()).To(Succeed()) + + return err + }) + Expect(err).NotTo(HaveOccurred()) + + containerWithTbfResult, err := current.GetResult(containerWithTbfRes) + Expect(err).NotTo(HaveOccurred()) + + tbfPluginConf := &PluginConf{} + err = json.Unmarshal([]byte(ptpConf), &tbfPluginConf) + Expect(err).NotTo(HaveOccurred()) + + tbfPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ + IngressBurst: burstInBits, + IngressRate: rateInBits, + EgressBurst: burstInBits, + EgressRate: rateInBits, + } + tbfPluginConf.Type = "bandwidth" + cniVersion := "0.4.0" + _, newConfBytes, err := buildOneConfig("myBWnet", cniVersion, tbfPluginConf, containerWithTbfResult) + Expect(err).NotTo(HaveOccurred()) + + args := &skel.CmdArgs{ + ContainerID: "dummy3", + Netns: containerWithTbfNS.Path(), + IfName: containerWithTbfIFName, + StdinData: newConfBytes, + } + + result, out, err := testutils.CmdAdd(containerWithTbfNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) }) + Expect(err).NotTo(HaveOccurred(), string(out)) + + // Do CNI Check + checkConf := &PluginConf{} + err = json.Unmarshal([]byte(ptpConf), &checkConf) + Expect(err).NotTo(HaveOccurred()) + + checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ + IngressBurst: burstInBits, + IngressRate: rateInBits, + EgressBurst: burstInBits, + EgressRate: rateInBits, + } + checkConf.Type = "bandwidth" + + _, newCheckBytes, err := buildOneConfig("myBWnet", cniVersion, checkConf, result) + Expect(err).NotTo(HaveOccurred()) + + args = &skel.CmdArgs{ + ContainerID: "dummy3", + Netns: containerWithTbfNS.Path(), + IfName: containerWithTbfIFName, + StdinData: newCheckBytes, + } + + err = testutils.CmdCheck(containerWithTbfNS.Path(), args.ContainerID, "", newCheckBytes, func() error { return cmdCheck(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + })).To(Succeed()) + + By("starting a tcp server on both containers") + portServerWithTbf, echoServerWithTbf, err = startEchoServerInNamespace(containerWithTbfNS) + Expect(err).NotTo(HaveOccurred()) + portServerWithoutTbf, echoServerWithoutTbf, err = startEchoServerInNamespace(containerWithoutTbfNS) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + containerWithTbfNS.Close() + containerWithoutTbfNS.Close() + if echoServerWithoutTbf != nil { + echoServerWithoutTbf.Kill() + } + if echoServerWithTbf != nil { + echoServerWithTbf.Kill() + } + }) + + Measure("limits ingress traffic on veth device", func(b Benchmarker) { + var runtimeWithLimit time.Duration + var runtimeWithoutLimit time.Duration + + By("gather timing statistics about both containers") + By("sending tcp traffic to the container that has traffic shaped", func() { + runtimeWithLimit = b.Time("with tbf", func() { + result, err := current.GetResult(containerWithTbfRes) + Expect(err).NotTo(HaveOccurred()) + + makeTcpClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithTbf, packetInBytes) + }) + }) + + By("sending tcp traffic to the container that does not have traffic shaped", func() { + runtimeWithoutLimit = b.Time("without tbf", func() { + result, err := current.GetResult(containerWithoutTbfRes) + Expect(err).NotTo(HaveOccurred()) + + makeTcpClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutTbf, packetInBytes) + }) + }) + + Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond)) + }, 1) + }) + }) diff --git a/plugins/meta/bandwidth/main.go b/plugins/meta/bandwidth/main.go index 5a2e5630..47bb6aca 100644 --- a/plugins/meta/bandwidth/main.go +++ b/plugins/meta/bandwidth/main.go @@ -50,10 +50,6 @@ type PluginConf struct { Bandwidth *BandwidthEntry `json:"bandwidth,omitempty"` } `json:"runtimeConfig,omitempty"` - // RuntimeConfig *struct{} `json:"runtimeConfig"` - - RawPrevResult *map[string]interface{} `json:"prevResult"` - PrevResult *current.Result `json:"-"` *BandwidthEntry } @@ -65,21 +61,6 @@ func parseConfig(stdin []byte) (*PluginConf, error) { return nil, fmt.Errorf("failed to parse network configuration: %v", err) } - if conf.RawPrevResult != nil { - resultBytes, err := json.Marshal(conf.RawPrevResult) - if err != nil { - return nil, fmt.Errorf("could not serialize prevResult: %v", err) - } - res, err := version.NewResult(conf.CNIVersion, resultBytes) - if err != nil { - return nil, fmt.Errorf("could not parse prevResult: %v", err) - } - conf.RawPrevResult = nil - conf.PrevResult, err = current.NewResultFromResult(res) - if err != nil { - return nil, fmt.Errorf("could not convert result to current version: %v", err) - } - } bandwidth := getBandwidth(&conf) if bandwidth != nil { err := validateRateAndBurst(bandwidth.IngressRate, bandwidth.IngressBurst) @@ -92,6 +73,18 @@ func parseConfig(stdin []byte) (*PluginConf, error) { } } + if conf.RawPrevResult != nil { + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, fmt.Errorf("could not parse prevResult: %v", err) + } + + _, err = current.NewResultFromResult(conf.PrevResult) + if err != nil { + return nil, fmt.Errorf("could not convert result to current version: %v", err) + } + } + return &conf, nil } @@ -167,7 +160,11 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("must be called as chained plugin") } - hostInterface, err := getHostInterface(conf.PrevResult.Interfaces) + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return fmt.Errorf("could not convert result to current version: %v", err) + } + hostInterface, err := getHostInterface(result.Interfaces) if err != nil { return err } @@ -200,7 +197,7 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - conf.PrevResult.Interfaces = append(conf.PrevResult.Interfaces, ¤t.Interface{ + result.Interfaces = append(result.Interfaces, ¤t.Interface{ Name: ifbDeviceName, Mac: ifbDevice.Attrs().HardwareAddr.String(), }) @@ -210,7 +207,7 @@ func cmdAdd(args *skel.CmdArgs) error { } } - return types.PrintResult(conf.PrevResult, conf.CNIVersion) + return types.PrintResult(result, conf.CNIVersion) } func cmdDel(args *skel.CmdArgs) error { @@ -233,10 +230,125 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.3.0", "0.3.1", version.Current()), "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.PluginSupports("0.3.0", "0.3.1", version.Current()), "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func SafeQdiscList(link netlink.Link) ([]netlink.Qdisc, error) { + qdiscs, err := netlink.QdiscList(link) + if err != nil { + return nil, err + } + result := []netlink.Qdisc{} + for _, qdisc := range qdiscs { + // filter out pfifo_fast qdiscs because + // older kernels don't return them + _, pfifo := qdisc.(*netlink.PfifoFast) + if !pfifo { + result = append(result, qdisc) + } + } + return result, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + bwConf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + + if bwConf.PrevResult == nil { + return fmt.Errorf("must be called as a chained plugin") + } + + result, err := current.NewResultFromResult(bwConf.PrevResult) + if err != nil { + return fmt.Errorf("could not convert result to current version: %v", err) + } + + hostInterface, err := getHostInterface(result.Interfaces) + if err != nil { + return err + } + link, err := netlink.LinkByName(hostInterface.Name) + if err != nil { + return err + } + + bandwidth := getBandwidth(bwConf) + + if bandwidth.IngressRate > 0 && bandwidth.IngressBurst > 0 { + rateInBytes := bandwidth.IngressRate / 8 + burstInBytes := bandwidth.IngressBurst / 8 + bufferInBytes := buffer(uint64(rateInBytes), uint32(burstInBytes)) + latency := latencyInUsec(latencyInMillis) + limitInBytes := limit(uint64(rateInBytes), latency, uint32(burstInBytes)) + + qdiscs, err := SafeQdiscList(link) + if err != nil { + return err + } + if len(qdiscs) == 0 { + return fmt.Errorf("Failed to find qdisc") + } + + for _, qdisc := range qdiscs { + tbf, isTbf := qdisc.(*netlink.Tbf) + if !isTbf { + break + } + if tbf.Rate != uint64(rateInBytes) { + return fmt.Errorf("Rate doesn't match") + } + if tbf.Limit != uint32(limitInBytes) { + return fmt.Errorf("Limit doesn't match") + } + if tbf.Buffer != uint32(bufferInBytes) { + return fmt.Errorf("Buffer doesn't match") + } + } + } + + if bandwidth.EgressRate > 0 && bandwidth.EgressBurst > 0 { + rateInBytes := bandwidth.EgressRate / 8 + burstInBytes := bandwidth.EgressBurst / 8 + bufferInBytes := buffer(uint64(rateInBytes), uint32(burstInBytes)) + latency := latencyInUsec(latencyInMillis) + limitInBytes := limit(uint64(rateInBytes), latency, uint32(burstInBytes)) + + ifbDeviceName, err := getIfbDeviceName(bwConf.Name, args.ContainerID) + if err != nil { + return err + } + + ifbDevice, err := netlink.LinkByName(ifbDeviceName) + if err != nil { + return fmt.Errorf("get ifb device: %s", err) + } + + qdiscs, err := SafeQdiscList(ifbDevice) + if err != nil { + return err + } + if len(qdiscs) == 0 { + return fmt.Errorf("Failed to find qdisc") + } + + for _, qdisc := range qdiscs { + tbf, isTbf := qdisc.(*netlink.Tbf) + if !isTbf { + break + } + if tbf.Rate != uint64(rateInBytes) { + return fmt.Errorf("Rate doesn't match") + } + if tbf.Limit != uint32(limitInBytes) { + return fmt.Errorf("Limit doesn't match") + } + if tbf.Buffer != uint32(bufferInBytes) { + return fmt.Errorf("Buffer doesn't match") + } + } + } + + return nil } diff --git a/plugins/meta/portmap/chain.go b/plugins/meta/portmap/chain.go index 9800e03f..bca8214a 100644 --- a/plugins/meta/portmap/chain.go +++ b/plugins/meta/portmap/chain.go @@ -138,3 +138,44 @@ func chainExists(ipt *iptables.IPTables, tableName, chainName string) (bool, err } return false, nil } + +// check the chain. +func (c *chain) check(ipt *iptables.IPTables) error { + + exists, err := chainExists(ipt, c.table, c.name) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("chain %s not found in iptables table %s", c.name, c.table) + } + + for i := len(c.rules) - 1; i >= 0; i-- { + match := checkRule(ipt, c.table, c.name, c.rules[i]) + if !match { + return fmt.Errorf("rule %s in chain %s not found in table %s", c.rules, c.name, c.table) + } + } + + for _, entryChain := range c.entryChains { + for i := len(c.entryRules) - 1; i >= 0; i-- { + r := []string{} + r = append(r, c.entryRules[i]...) + r = append(r, "-j", c.name) + matchEntryChain := checkRule(ipt, c.table, entryChain, r) + if !matchEntryChain { + return fmt.Errorf("rule %s in chain %s not found in table %s", c.entryRules, entryChain, c.table) + } + } + } + + return nil +} + +func checkRule(ipt *iptables.IPTables, table, chain string, rule []string) bool { + exists, err := ipt.Exists(table, chain, rule...) + if err != nil { + return false + } + return exists +} diff --git a/plugins/meta/portmap/main.go b/plugins/meta/portmap/main.go index 2f44bc95..2737186b 100644 --- a/plugins/meta/portmap/main.go +++ b/plugins/meta/portmap/main.go @@ -55,8 +55,6 @@ type PortMapConf struct { RuntimeConfig struct { PortMaps []PortMapEntry `json:"portMappings,omitempty"` } `json:"runtimeConfig,omitempty"` - RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` - PrevResult *current.Result `json:"-"` // These are fields parsed out of the config or the environment; // included here for convenience @@ -70,7 +68,7 @@ type PortMapConf struct { const DefaultMarkBit = 13 func cmdAdd(args *skel.CmdArgs) error { - netConf, err := parseConfig(args.StdinData, args.IfName) + netConf, _, err := parseConfig(args.StdinData, args.IfName) if err != nil { return fmt.Errorf("failed to parse config: %v", err) } @@ -102,7 +100,7 @@ func cmdAdd(args *skel.CmdArgs) error { } func cmdDel(args *skel.CmdArgs) error { - netConf, err := parseConfig(args.StdinData, args.IfName) + netConf, _, err := parseConfig(args.StdinData, args.IfName) if err != nil { return fmt.Errorf("failed to parse config: %v", err) } @@ -119,36 +117,60 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf, result, err := parseConfig(args.StdinData, args.IfName) + if err != nil { + return err + } + + // Ensure we have previous result. + if result == nil { + return fmt.Errorf("Required prevResult missing") + } + + if len(conf.RuntimeConfig.PortMaps) == 0 { + return nil + } + + conf.ContainerID = args.ContainerID + + if conf.ContIPv4 != nil { + if err := checkPorts(conf, conf.ContIPv4); err != nil { + return err + } + } + + if conf.ContIPv6 != nil { + if err := checkPorts(conf, conf.ContIPv6); err != nil { + return err + } + } + + return nil } // parseConfig parses the supplied configuration (and prevResult) from stdin. -func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { +func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, error) { conf := PortMapConf{} if err := json.Unmarshal(stdin, &conf); err != nil { - return nil, fmt.Errorf("failed to parse network configuration: %v", err) + return nil, nil, fmt.Errorf("failed to parse network configuration: %v", err) } // Parse previous result. + var result *current.Result if conf.RawPrevResult != nil { - resultBytes, err := json.Marshal(conf.RawPrevResult) - if err != nil { - return nil, fmt.Errorf("could not serialize prevResult: %v", err) + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, nil, fmt.Errorf("could not parse prevResult: %v", err) } - res, err := version.NewResult(conf.CNIVersion, resultBytes) + + result, err = current.NewResultFromResult(conf.PrevResult) if err != nil { - return nil, fmt.Errorf("could not parse prevResult: %v", err) - } - conf.RawPrevResult = nil - conf.PrevResult, err = current.NewResultFromResult(res) - if err != nil { - return nil, fmt.Errorf("could not convert result to current version: %v", err) + return nil, nil, fmt.Errorf("could not convert result to current version: %v", err) } } @@ -158,7 +180,7 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } if conf.MarkMasqBit != nil && conf.ExternalSetMarkChain != nil { - return nil, fmt.Errorf("Cannot specify externalSetMarkChain and markMasqBit") + return nil, nil, fmt.Errorf("Cannot specify externalSetMarkChain and markMasqBit") } if conf.MarkMasqBit == nil { @@ -167,21 +189,21 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } if *conf.MarkMasqBit < 0 || *conf.MarkMasqBit > 31 { - return nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") + return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") } // Reject invalid port numbers for _, pm := range conf.RuntimeConfig.PortMaps { if pm.ContainerPort <= 0 { - return nil, fmt.Errorf("Invalid container port number: %d", pm.ContainerPort) + return nil, nil, fmt.Errorf("Invalid container port number: %d", pm.ContainerPort) } if pm.HostPort <= 0 { - return nil, fmt.Errorf("Invalid host port number: %d", pm.HostPort) + return nil, nil, fmt.Errorf("Invalid host port number: %d", pm.HostPort) } } if conf.PrevResult != nil { - for _, ip := range conf.PrevResult.IPs { + for _, ip := range result.IPs { if ip.Version == "6" && conf.ContIPv6 != nil { continue } else if ip.Version == "4" && conf.ContIPv4 != nil { @@ -192,9 +214,9 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { if ip.Interface != nil { intIdx := *ip.Interface if intIdx >= 0 && - intIdx < len(conf.PrevResult.Interfaces) && - (conf.PrevResult.Interfaces[intIdx].Name != ifName || - conf.PrevResult.Interfaces[intIdx].Sandbox == "") { + intIdx < len(result.Interfaces) && + (result.Interfaces[intIdx].Name != ifName || + result.Interfaces[intIdx].Sandbox == "") { continue } } @@ -207,5 +229,5 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } } - return &conf, nil + return &conf, result, nil } diff --git a/plugins/meta/portmap/portmap.go b/plugins/meta/portmap/portmap.go index e57622b6..d73dda20 100644 --- a/plugins/meta/portmap/portmap.go +++ b/plugins/meta/portmap/portmap.go @@ -111,6 +111,46 @@ func forwardPorts(config *PortMapConf, containerIP net.IP) error { return nil } +func checkPorts(config *PortMapConf, containerIP net.IP) error { + + dnatChain := genDnatChain(config.Name, config.ContainerID) + fillDnatRules(&dnatChain, config, containerIP) + + ip4t := maybeGetIptables(false) + ip6t := maybeGetIptables(true) + if ip4t == nil && ip6t == nil { + return fmt.Errorf("neither iptables nor ip6tables usable") + } + + if ip4t != nil { + exists, err := chainExists(ip4t, dnatChain.table, dnatChain.name) + if err != nil { + return err + } + if !exists { + return err + } + if err := dnatChain.check(ip4t); err != nil { + return fmt.Errorf("could not check ipv4 dnat: %v", err) + } + } + + if ip6t != nil { + exists, err := chainExists(ip6t, dnatChain.table, dnatChain.name) + if err != nil { + return err + } + if !exists { + return err + } + if err := dnatChain.check(ip6t); err != nil { + return fmt.Errorf("could not check ipv6 dnat: %v", err) + } + } + + return nil +} + // genToplevelDnatChain creates the top-level summary chain that we'll // add our chain to. This is easy, because creating chains is idempotent. // IMPORTANT: do not change this, or else upgrading plugins will require diff --git a/plugins/meta/portmap/portmap_test.go b/plugins/meta/portmap/portmap_test.go index 44e2d936..8b04bf78 100644 --- a/plugins/meta/portmap/portmap_test.go +++ b/plugins/meta/portmap/portmap_test.go @@ -68,7 +68,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - c, err := parseConfig(configBytes, "container") + c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal("0.3.1")) Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) @@ -91,7 +91,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV4": ["a", "b"], "conditionsV6": ["c", "d"] }`) - c, err := parseConfig(configBytes, "container") + c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal("0.3.1")) Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) @@ -115,7 +115,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - _, err := parseConfig(configBytes, "container") + _, _, err := parseConfig(configBytes, "container") Expect(err).To(MatchError("Invalid host port number: 0")) }) @@ -143,7 +143,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - _, err := parseConfig(configBytes, "container") + _, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) }) }) @@ -175,7 +175,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV6": ["c", "d"] }`) - conf, err := parseConfig(configBytes, "foo") + conf, _, err := parseConfig(configBytes, "foo") Expect(err).NotTo(HaveOccurred()) conf.ContainerID = containerID @@ -271,7 +271,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV6": ["c", "d"] }`) - conf, err := parseConfig(configBytes, "foo") + conf, _, err := parseConfig(configBytes, "foo") Expect(err).NotTo(HaveOccurred()) conf.ContainerID = containerID diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go index 156a4b73..0e2b70b9 100644 --- a/plugins/meta/tuning/tuning.go +++ b/plugins/meta/tuning/tuning.go @@ -36,12 +36,10 @@ import ( // TuningConf represents the network tuning configuration. type TuningConf struct { types.NetConf - SysCtl map[string]string `json:"sysctl"` - RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` - PrevResult *current.Result `json:"-"` - Mac string `json:"mac,omitempty"` - Promisc bool `json:"promisc,omitempty"` - Mtu int `json:"mtu,omitempty"` + SysCtl map[string]string `json:"sysctl"` + Mac string `json:"mac,omitempty"` + Promisc bool `json:"promisc,omitempty"` + Mtu int `json:"mtu,omitempty"` } type MACEnvArgs struct { @@ -68,23 +66,6 @@ func parseConf(data []byte, envArgs string) (*TuningConf, error) { } } - // Parse previous result. - if conf.RawPrevResult != nil { - resultBytes, err := json.Marshal(conf.RawPrevResult) - if err != nil { - return nil, fmt.Errorf("could not serialize prevResult: %v", err) - } - res, err := version.NewResult(conf.CNIVersion, resultBytes) - if err != nil { - return nil, fmt.Errorf("could not parse prevResult: %v", err) - } - conf.RawPrevResult = nil - conf.PrevResult, err = current.NewResultFromResult(res) - if err != nil { - return nil, fmt.Errorf("could not convert result to current version: %v", err) - } - } - return &conf, nil } @@ -111,7 +92,15 @@ func changeMacAddr(ifName string, newMacAddr string) error { } func updateResultsMacAddr(config TuningConf, ifName string, newMacAddr string) { - for _, i := range config.PrevResult.Interfaces { + // Parse previous result. + if config.PrevResult == nil { + return + } + + version.ParsePrevResult(&config.NetConf) + result, _ := current.NewResultFromResult(config.PrevResult) + + for _, i := range result.Interfaces { if i.Name == ifName { i.Mac = newMacAddr } @@ -144,6 +133,20 @@ func cmdAdd(args *skel.CmdArgs) error { return err } + // Parse previous result. + if tuningConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&tuningConf.NetConf); err != nil { + return err + } + + _, err = current.NewResultFromResult(tuningConf.PrevResult) + if err != nil { + return err + } + // The directory /proc/sys/net is per network namespace. Enter in the // network namespace before writing on it. @@ -200,10 +203,80 @@ func cmdDel(args *skel.CmdArgs) error { func main() { // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, "TODO") } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + tuningConf, err := parseConf(args.StdinData, args.Args) + if err != nil { + return err + } + + // Parse previous result. + if tuningConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&tuningConf.NetConf); err != nil { + return err + } + + _, err = current.NewResultFromResult(tuningConf.PrevResult) + if err != nil { + return err + } + + err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { + // Check each configured value vs what's currently in the container + for key, conf_value := range tuningConf.SysCtl { + fileName := filepath.Join("/proc/sys", strings.Replace(key, ".", "/", -1)) + fileName = filepath.Clean(fileName) + + contents, err := ioutil.ReadFile(fileName) + if err != nil { + return err + } + cur_value := strings.TrimSuffix(string(contents), "\n") + if conf_value != cur_value { + return fmt.Errorf("Error: Tuning configured value of %s is %s, current value is %s", fileName, conf_value, cur_value) + } + } + + link, err := netlink.LinkByName(args.IfName) + if err != nil { + return fmt.Errorf("Cannot find container link %v", args.IfName) + } + + if tuningConf.Mac != "" { + if tuningConf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Error: Tuning configured Ethernet of %s is %s, current value is %s", + args.IfName, tuningConf.Mac, link.Attrs().HardwareAddr) + } + } + + if tuningConf.Promisc { + if link.Attrs().Promisc == 0 { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + args.IfName, tuningConf.Promisc, link.Attrs().Promisc) + } + } else { + if link.Attrs().Promisc != 0 { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + args.IfName, tuningConf.Promisc, link.Attrs().Promisc) + } + } + + if tuningConf.Mtu != 0 { + if tuningConf.Mtu != link.Attrs().MTU { + return fmt.Errorf("Error: Tuning configured MTU of %s is %d, current value is %d", + args.IfName, tuningConf.Mtu, link.Attrs().MTU) + } + } + return nil + }) + if err != nil { + return err + } + + return nil } diff --git a/plugins/meta/tuning/tuning_test.go b/plugins/meta/tuning/tuning_test.go index 1b68bf15..87dd2619 100644 --- a/plugins/meta/tuning/tuning_test.go +++ b/plugins/meta/tuning/tuning_test.go @@ -15,7 +15,11 @@ package main import ( + "encoding/json" + "fmt" + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/testutils" @@ -27,6 +31,49 @@ import ( . "github.com/onsi/gomega" ) +func buildOneConfig(name, cniVersion string, orig *TuningConf, prevResult types.Result) (*TuningConf, []byte, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, nil, err + } + + conf := &TuningConf{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, newBytes, nil + +} + var _ = Describe("tuning plugin", func() { var originalNS ns.NetNS const IFNAME string = "dummy0" @@ -342,4 +389,296 @@ var _ = Describe("tuning plugin", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures promiscuous mode with CNI 0.4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "promisc": true, + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, 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)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Promisc).To(Equal(1)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mtu with CNI 0.4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "mtu": 1454, + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, 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)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().MTU).To(Equal(1454)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mac address (from conf file) with CNI v4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "mac": "c2:11:22:33:44:55", + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, 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)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + hw, err := net.ParseMAC("c2:11:22:33:44:55") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hw)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mac address (from CNI_ARGS) with CNI v4 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + Args: "IgnoreUnknown=true;MAC=c2:11:22:33:44:66", + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, 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)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + hw, err := net.ParseMAC("c2:11:22:33:44:66") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hw)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) })