From c3ff1238c113264a36a7ebb3f9652f0b2f2570f9 Mon Sep 17 00:00:00 2001 From: Michael C Cambria Date: Thu, 18 May 2017 19:36:05 +0000 Subject: [PATCH] Initial anycast plugin --- build.sh | 2 +- plugins/anycast/README.md | 7 + plugins/anycast/main.go | 252 +++++++++++++++++++++++++++ plugins/anycast/main.go~ | 252 +++++++++++++++++++++++++++ plugins/anycast/sample_suite_test.go | 15 ++ plugins/anycast/sample_test.go | 138 +++++++++++++++ 6 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 plugins/anycast/README.md create mode 100644 plugins/anycast/main.go create mode 100644 plugins/anycast/main.go~ create mode 100644 plugins/anycast/sample_suite_test.go create mode 100644 plugins/anycast/sample_test.go diff --git a/build.sh b/build.sh index b1b82a6..8ba8b41 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,7 @@ export GOPATH=${PWD}/gopath mkdir -p "${PWD}/bin" echo "Building plugins" -PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample" +PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/anycast plugins/sample" for d in $PLUGINS; do if [ -d "$d" ]; then plugin="$(basename "$d")" diff --git a/plugins/anycast/README.md b/plugins/anycast/README.md new file mode 100644 index 0000000..9bbd6e9 --- /dev/null +++ b/plugins/anycast/README.md @@ -0,0 +1,7 @@ +# Sample CNI plugin + +This is an example of a sample chained plugin. It includes solutions for some +of the more subtle cases that can be experienced with multi-version chained +plugins. + +To use it, just add your code to the cmdAdd and cmdDel plugins. diff --git a/plugins/anycast/main.go b/plugins/anycast/main.go new file mode 100644 index 0000000..01f8f8d --- /dev/null +++ b/plugins/anycast/main.go @@ -0,0 +1,252 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a sample chained plugin that supports multiple CNI versions. It +// parses prevResult according to the cniVersion +package main + +import ( + "encoding/json" + "fmt" + "net" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/version" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/vishvananda/netlink" + "log" + "os" + "syscall" +) + +// PluginConf is whatever you expect your configuration json to be. This is whatever +// is passed in on stdin. Your plugin may wish to expose its functionality via +// runtime args, see CONVENTIONS.md in the CNI spec. +type PluginConf struct { + types.NetConf // You may wish to not nest this type + RuntimeConfig *struct { + SampleConfig map[string]interface{} `json:"sample"` + } `json:"runtimeConfig"` + + // This is the previous result, when called in the context of a chained + // plugin. Because this plugin supports multiple versions, we'll have to + // parse this in two passes. If your plugin is not chained, this can be + // removed (though you may wish to error if a non-chainable plugin is + // chained. + // If you need to modify the result before returning it, you will need + // to actually convert it to a concrete versioned struct. + RawPrevResult *map[string]interface{} `json:"prevResult"` + PrevResult *current.Result `json:"-"` + + // Add plugin-specifc flags here + MyAwesomeFlag bool `json:"myAwesomeFlag"` + AnotherAwesomeArg string `json:"anotherAwesomeArg"` + STATICIP net.IP `json:"StaticIP,omitempty"` + VIAIP net.IP `json:"viaIP,omitempty"` + UPLINK types.UnmarshallableString `json:"uplink,omitempty"` +} + +// parseConfig parses the supplied configuration (and prevResult) from stdin. +func parseConfig(stdin []byte) (*PluginConf, error) { + conf := PluginConf{} + + if err := json.Unmarshal(stdin, &conf); err != nil { + return nil, fmt.Errorf("failed to parse network configuration: %v", err) + } + + // Parse previous result. Remove this if your plugin is not chained. + 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) + } + } + // End previous result parsing + + // Do any validation here + if conf.AnotherAwesomeArg == "" { + return nil, fmt.Errorf("anotherAwesomeArg must be specified") + } + + return &conf, nil +} + +// cmdAdd is called for ADD requests +func cmdAdd(args *skel.CmdArgs) error { + conf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + + if conf.PrevResult == nil { + return fmt.Errorf("must be called as chained plugin") + } + + // This is some sample code to generate the list of container-side IPs. + // We're casting the prevResult to a 0.3.1 response, which can also include + // host-side IPs (but doesn't when converted from a 0.2.0 response). + containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs)) + if conf.CNIVersion != "0.3.1" { + for _, ip := range conf.PrevResult.IPs { + containerIPs = append(containerIPs, ip.Address.IP) + log.Println("xIP 1 is: ", ip.Address.IP) + log.Println("xIP 2 is: ", ip) + } + } else { + for _, ip := range conf.PrevResult.IPs { + intIdx := ip.Interface + // Every IP is indexed in to the interfaces array, with "-1" standing + // for an unknown interface (which we'll assume to be Container-side + // Skip all IPs we know belong to an interface with the wrong name. + if intIdx >= 0 && intIdx < len(conf.PrevResult.Interfaces) && conf.PrevResult.Interfaces[intIdx].Name != args.IfName { + log.Println("skip: 1 ", ip.Address.IP) + log.Println("skip: 2", ip) + continue + } + containerIPs = append(containerIPs, ip.Address.IP) + log.Println("no skip IP 1 is: ", ip.Address.IP) + log.Println("no skip IP 2 is: ", ip) +} + } + if len(containerIPs) == 0 { + return fmt.Errorf("got no container IPs") + } + + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + + log.Printf("Slice IPs: len=%d, cap=%d values= %v\n", + len(containerIPs), + cap(containerIPs), + containerIPs) + + viaIP := conf.VIAIP + log.Println("viaIP = ", viaIP) + + uplink := conf.UPLINK + log.Println("uplink = ", uplink) + + staticIP := conf.STATICIP + log.Println("staticIP = ", staticIP) + + log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink) + + dst := &net.IPNet{ + IP: staticIP, + Mask: net.CIDRMask(32, 32), + } + + link, err := netlink.LinkByName(string(uplink)) + if err != nil { + log.Println("Can't obtain link index for: ", uplink) + return err + } + + route := netlink.Route{ + Dst: dst, + LinkIndex: link.Attrs().Index, + Gw: viaIP, + } + + if err := netlink.RouteAdd(&route); err != nil { + fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err) + if (err == syscall.EAGAIN) { + log.Println("ERRNO: eagain") + } else if (err == syscall.EEXIST) { + log.Println("ERRNO: route already exists") + } else { + log.Println("ERRNO: value is: ", (int(err.(syscall.Errno)))) + } + return err + } + + // Pass trough the result for the next plugin + return types.PrintResult(conf.PrevResult, conf.CNIVersion) +} + +// cmdDel is called for DELETE requests +func cmdDel(args *skel.CmdArgs) error { + conf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + _ = conf + + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + + viaIP := conf.VIAIP + log.Println("viaIP = ", viaIP) + + uplink := conf.UPLINK + log.Println("uplink = ", uplink) + + staticIP := conf.STATICIP + log.Println("staticIP = ", staticIP) + + log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink) + + dst := &net.IPNet{ + IP: staticIP, + Mask: net.CIDRMask(32, 32), + } + + link, err := netlink.LinkByName(string(uplink)) + if err != nil { + log.Println("Can't obtain link index for: ", uplink) + return err + } + + route := netlink.Route{ + Dst: dst, + LinkIndex: link.Attrs().Index, + Gw: viaIP, + } + + if err := netlink.RouteDel(&route); err != nil { + fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err) + if (err == syscall.EAGAIN) { + log.Println("ERRNO: eagain") + } else if (err == syscall.EEXIST) { + log.Println("ERRNO: route already exists") + } else { + log.Println("ERRNO: value is: ", (int(err.(syscall.Errno)))) + } + return err + } + + return nil +} + +func main() { + skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("", "0.1.0", "0.2.0", version.Current())) +} diff --git a/plugins/anycast/main.go~ b/plugins/anycast/main.go~ new file mode 100644 index 0000000..39ff639 --- /dev/null +++ b/plugins/anycast/main.go~ @@ -0,0 +1,252 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a sample chained plugin that supports multiple CNI versions. It +// parses prevResult according to the cniVersion +package main + +import ( + "encoding/json" + "fmt" + "net" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/version" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/vishvananda/netlink" + "log" + "os" + "syscall" +) + +// PluginConf is whatever you expect your configuration json to be. This is whatever +// is passed in on stdin. Your plugin may wish to expose its functionality via +// runtime args, see CONVENTIONS.md in the CNI spec. +type PluginConf struct { + types.NetConf // You may wish to not nest this type + RuntimeConfig *struct { + SampleConfig map[string]interface{} `json:"sample"` + } `json:"runtimeConfig"` + + // This is the previous result, when called in the context of a chained + // plugin. Because this plugin supports multiple versions, we'll have to + // parse this in two passes. If your plugin is not chained, this can be + // removed (though you may wish to error if a non-chainable plugin is + // chained. + // If you need to modify the result before returning it, you will need + // to actually convert it to a concrete versioned struct. + RawPrevResult *map[string]interface{} `json:"prevResult"` + PrevResult *current.Result `json:"-"` + + // Add plugin-specifc flags here + MyAwesomeFlag bool `json:"myAwesomeFlag"` + AnotherAwesomeArg string `json:"anotherAwesomeArg"` + STATICIP net.IP `json:"StaticIP,omitempty"` + VIAIP net.IP `json:"viaIP,omitempty"` + UPLINK types.UnmarshallableString `json:"uplink,omitempty"` +} + +// parseConfig parses the supplied configuration (and prevResult) from stdin. +func parseConfig(stdin []byte) (*PluginConf, error) { + conf := PluginConf{} + + if err := json.Unmarshal(stdin, &conf); err != nil { + return nil, fmt.Errorf("failed to parse network configuration: %v", err) + } + + // Parse previous result. Remove this if your plugin is not chained. + 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) + } + } + // End previous result parsing + + // Do any validation here + if conf.AnotherAwesomeArg == "" { + return nil, fmt.Errorf("anotherAwesomeArg must be specified") + } + + return &conf, nil +} + +// cmdAdd is called for ADD requests +func cmdAdd(args *skel.CmdArgs) error { + conf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + + if conf.PrevResult == nil { + return fmt.Errorf("must be called as chained plugin") + } + + // This is some sample code to generate the list of container-side IPs. + // We're casting the prevResult to a 0.3.1 response, which can also include + // host-side IPs (but doesn't when converted from a 0.2.0 response). + containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs)) + if conf.CNIVersion != "0.3.1" { + for _, ip := range conf.PrevResult.IPs { + containerIPs = append(containerIPs, ip.Address.IP) + log.Println("xIP 1 is: ", ip.Address.IP) + log.Println("xIP 2 is: ", ip) + } + } else { + for _, ip := range conf.PrevResult.IPs { + intIdx := ip.Interface + // Every IP is indexed in to the interfaces array, with "-1" standing + // for an unknown interface (which we'll assume to be Container-side + // Skip all IPs we know belong to an interface with the wrong name. + if intIdx >= 0 && intIdx < len(conf.PrevResult.Interfaces) && conf.PrevResult.Interfaces[intIdx].Name != args.IfName { + log.Println("skip: 1 ", ip.Address.IP) + log.Println("skip: 2", ip) + continue + } + containerIPs = append(containerIPs, ip.Address.IP) + log.Println("no skip IP 1 is: ", ip.Address.IP) + log.Println("no skip IP 2 is: ", ip) +} + } + if len(containerIPs) == 0 { + return fmt.Errorf("got no container IPs") + } + + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + + fmt.Printf("Slice IPs: len=%d, cap=%d values= %v\n", + len(containerIPs), + cap(containerIPs), + containerIPs) + + viaIP := conf.VIAIP + log.Println("viaIP = ", viaIP) + + uplink := conf.UPLINK + log.Println("uplink = ", uplink) + + staticIP := conf.STATICIP + log.Println("staticIP = ", staticIP) + + log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink) + + dst := &net.IPNet{ + IP: staticIP, + Mask: net.CIDRMask(32, 32), + } + + link, err := netlink.LinkByName(string(uplink)) + if err != nil { + log.Println("Can't obtain link index for: ", uplink) + return err + } + + route := netlink.Route{ + Dst: dst, + LinkIndex: link.Attrs().Index, + Gw: viaIP, + } + + if err := netlink.RouteAdd(&route); err != nil { + fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err) + if (err == syscall.EAGAIN) { + log.Println("ERRNO: eagain") + } else if (err == syscall.EEXIST) { + log.Println("ERRNO: route already exists") + } else { + log.Println("ERRNO: value is: ", (int(err.(syscall.Errno)))) + } + return err + } + + // Pass trough the result for the next plugin + return types.PrintResult(conf.PrevResult, conf.CNIVersion) +} + +// cmdDel is called for DELETE requests +func cmdDel(args *skel.CmdArgs) error { + conf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + _ = conf + + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + + viaIP := conf.VIAIP + log.Println("viaIP = ", viaIP) + + uplink := conf.UPLINK + log.Println("uplink = ", uplink) + + staticIP := conf.STATICIP + log.Println("staticIP = ", staticIP) + + log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink) + + dst := &net.IPNet{ + IP: staticIP, + Mask: net.CIDRMask(32, 32), + } + + link, err := netlink.LinkByName(string(uplink)) + if err != nil { + log.Println("Can't obtain link index for: ", uplink) + return err + } + + route := netlink.Route{ + Dst: dst, + LinkIndex: link.Attrs().Index, + Gw: viaIP, + } + + if err := netlink.RouteDel(&route); err != nil { + fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err) + if (err == syscall.EAGAIN) { + log.Println("ERRNO: eagain") + } else if (err == syscall.EEXIST) { + log.Println("ERRNO: route already exists") + } else { + log.Println("ERRNO: value is: ", (int(err.(syscall.Errno)))) + } + return err + } + + return nil +} + +func main() { + skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("", "0.1.0", "0.2.0", version.Current())) +} diff --git a/plugins/anycast/sample_suite_test.go b/plugins/anycast/sample_suite_test.go new file mode 100644 index 0000000..f792c1e --- /dev/null +++ b/plugins/anycast/sample_suite_test.go @@ -0,0 +1,15 @@ +// The boilerplate needed for Ginkgo + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSample(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "sample suite") +} diff --git a/plugins/anycast/sample_test.go b/plugins/anycast/sample_test.go new file mode 100644 index 0000000..9d2688c --- /dev/null +++ b/plugins/anycast/sample_test.go @@ -0,0 +1,138 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/testutils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("sample test", func() { + var targetNs ns.NetNS + + BeforeEach(func() { + var err error + targetNs, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + targetNs.Close() + }) + + It("Works with a 0.3.0 config", func() { + ifname := "eth0" + conf := `{ + "cniVersion": "0.3.0", + "name": "cni-plugin-sample-test", + "type": "sample", + "anotherAwesomeArg": "awesome", + "prevResult": { + "interfaces": [ + { + "name": "%s", + "sandbox": "%s" + } + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ], + "routes": [] + } +}` + conf = fmt.Sprintf(conf, ifname, targetNs.Path()) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: ifname, + StdinData: []byte(conf), + } + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) }) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("fails an invalid config", func() { + conf := `{ + "cniVersion": "0.3.0", + "name": "cni-plugin-sample-test", + "type": "sample", + "prevResult": { + "interfaces": [ + { + "name": "eth0", + "sandbox": "/var/run/netns/test" + } + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ], + "routes": [] + } +}` + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: "eth0", + StdinData: []byte(conf), + } + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) }) + Expect(err).To(MatchError("anotherAwesomeArg must be specified")) + + }) + + It("works with a 0.2.0 config", func() { + conf := `{ + "cniVersion": "0.2.0", + "name": "cni-plugin-sample-test", + "type": "sample", + "anotherAwesomeArg": "foo", + "prevResult": { + "ip4": { + "ip": "10.0.0.2/24", + "gateway": "10.0.0.1", + "routes": [] + } + } +}` + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: "eth0", + StdinData: []byte(conf), + } + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) }) + Expect(err).NotTo(HaveOccurred()) + + }) + +}) -- 2.44.0