From 6099d8c84c7c43ef15139d6759c2436eb527bf23 Mon Sep 17 00:00:00 2001 From: Tino Rusch Date: Fri, 28 Apr 2017 09:34:22 +0200 Subject: [PATCH 1/3] added host-device plugin which adds a specified link to the container network namespace; --- plugins/host-device/host-device.go | 106 ++++++++++++++++++ plugins/host-device/host-device_suite_test.go | 27 +++++ plugins/host-device/host-device_test.go | 77 +++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 plugins/host-device/host-device.go create mode 100644 plugins/host-device/host-device_suite_test.go create mode 100644 plugins/host-device/host-device_test.go diff --git a/plugins/host-device/host-device.go b/plugins/host-device/host-device.go new file mode 100644 index 00000000..a4870857 --- /dev/null +++ b/plugins/host-device/host-device.go @@ -0,0 +1,106 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "fmt" + "runtime" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/version" + "github.com/vishvananda/netlink" +) + +type NetConf struct { + Device string `json:"device"` +} + +func init() { + // this ensures that main runs only on main thread (thread group leader). + // since namespace ops (unshare, setns) are done for a single thread, we + // must ensure that the goroutine does not jump from OS thread to thread + runtime.LockOSThread() +} + +func loadConf(bytes []byte) (*NetConf, error) { + n := &NetConf{} + if err := json.Unmarshal(bytes, n); err != nil { + return nil, fmt.Errorf("failed to load netconf: %v", err) + } + if n.Device == "" { + return nil, fmt.Errorf(`"device" field is required. It specifies the host device to put into the pod`) + } + return n, nil +} + +func cmdAdd(args *skel.CmdArgs) error { + cfg, err := loadConf(args.StdinData) + if err != nil { + return err + } + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + return addLink(cfg.Device, containerNs) +} + +func cmdDel(args *skel.CmdArgs) error { + cfg, err := loadConf(args.StdinData) + if err != nil { + return err + } + containerNs, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer containerNs.Close() + return removeLink(cfg.Device, containerNs) +} + +func addLink(name string, containerNs ns.NetNS) error { + dev, err := netlink.LinkByName(name) + if err != nil { + return fmt.Errorf("failed to lookup %v: %v", name, err) + } + return netlink.LinkSetNsFd(dev, int(containerNs.Fd())) +} + +func removeLink(name string, containerNs ns.NetNS) error { + var dev netlink.Link + err := containerNs.Do(func(_ ns.NetNS) error { + d, err := netlink.LinkByName(name) + if err != nil { + return err + } + dev = d + return nil + }) + if err != nil { + return err + } + defaultNs, err := ns.GetCurrentNS() + if err != nil { + return err + } + return netlink.LinkSetNsFd(dev, int(defaultNs.Fd())) +} + +func main() { + skel.PluginMain(cmdAdd, cmdDel, version.All) +} diff --git a/plugins/host-device/host-device_suite_test.go b/plugins/host-device/host-device_suite_test.go new file mode 100644 index 00000000..9a1702c7 --- /dev/null +++ b/plugins/host-device/host-device_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestVlan(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "host-device Suite") +} diff --git a/plugins/host-device/host-device_test.go b/plugins/host-device/host-device_test.go new file mode 100644 index 00000000..0f38a094 --- /dev/null +++ b/plugins/host-device/host-device_test.go @@ -0,0 +1,77 @@ +// 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("base functionality", 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 valid config", func() { + ifname := "eth0" + conf := `{ + "name": "cni-plugin-host-device-test", + "type": "host-device", + "device": "eth0" +}` + 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": "host-device" +}` + + 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")) + + }) + +}) From f2faf549b4b452303614b0917d71e43f56cfff49 Mon Sep 17 00:00:00 2001 From: Tino Rusch Date: Tue, 2 May 2017 13:21:50 +0200 Subject: [PATCH 2/3] [host-device] integrated `getLink()` function which maps either devicename, hw-addr or kernelpath to a link object; --- plugins/host-device/host-device.go | 72 +++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/plugins/host-device/host-device.go b/plugins/host-device/host-device.go index a4870857..853cab9e 100644 --- a/plugins/host-device/host-device.go +++ b/plugins/host-device/host-device.go @@ -15,9 +15,14 @@ package main import ( + "bytes" "encoding/json" "fmt" + "io/ioutil" + "net" + "path/filepath" "runtime" + "strings" "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" @@ -26,7 +31,9 @@ import ( ) type NetConf struct { - Device string `json:"device"` + 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 } func init() { @@ -41,8 +48,8 @@ func loadConf(bytes []byte) (*NetConf, error) { if err := json.Unmarshal(bytes, n); err != nil { return nil, fmt.Errorf("failed to load netconf: %v", err) } - if n.Device == "" { - return nil, fmt.Errorf(`"device" field is required. It specifies the host device to put into the pod`) + if n.Device == "" && n.HWAddr == "" && n.KernelPath == "" { + return nil, fmt.Errorf(`specify either "device", "hwaddr" or "kernelpath"`) } return n, nil } @@ -57,7 +64,7 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) } defer containerNs.Close() - return addLink(cfg.Device, containerNs) + return addLink(cfg.Device, cfg.HWAddr, cfg.KernelPath, containerNs) } func cmdDel(args *skel.CmdArgs) error { @@ -70,21 +77,21 @@ func cmdDel(args *skel.CmdArgs) error { return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) } defer containerNs.Close() - return removeLink(cfg.Device, containerNs) + return removeLink(cfg.Device, cfg.HWAddr, cfg.KernelPath, containerNs) } -func addLink(name string, containerNs ns.NetNS) error { - dev, err := netlink.LinkByName(name) +func addLink(device, hwAddr, kernelPath string, containerNs ns.NetNS) error { + dev, err := getLink(device, hwAddr, kernelPath) if err != nil { - return fmt.Errorf("failed to lookup %v: %v", name, err) + return err } return netlink.LinkSetNsFd(dev, int(containerNs.Fd())) } -func removeLink(name string, containerNs ns.NetNS) error { +func removeLink(device, hwAddr, kernelPath string, containerNs ns.NetNS) error { var dev netlink.Link err := containerNs.Do(func(_ ns.NetNS) error { - d, err := netlink.LinkByName(name) + d, err := getLink(device, hwAddr, kernelPath) if err != nil { return err } @@ -101,6 +108,51 @@ func removeLink(name string, containerNs ns.NetNS) error { return netlink.LinkSetNsFd(dev, int(defaultNs.Fd())) } +func getLink(devname, hwaddr, kernelpath string) (netlink.Link, error) { + links, err := netlink.LinkList() + if err != nil { + return nil, fmt.Errorf("failed to list node links: %v", err) + } + + if len(devname) > 0 { + if m, err := netlink.LinkByName(devname); err == nil { + return m, nil + } + } else if len(hwaddr) > 0 { + hwAddr, err := net.ParseMAC(hwaddr) + if err != nil { + return nil, fmt.Errorf("failed to parse MAC address %q: %v", hwaddr, err) + } + + for _, link := range links { + if bytes.Equal(link.Attrs().HardwareAddr, hwAddr) { + return link, nil + } + } + } else if len(kernelpath) > 0 { + if !filepath.IsAbs(kernelpath) || !strings.HasPrefix(kernelpath, "/sys/devices/") { + return nil, fmt.Errorf("kernel device path %q must be absolute and begin with /sys/devices/", kernelpath) + } + netDir := filepath.Join(kernelpath, "net") + files, err := ioutil.ReadDir(netDir) + if err != nil { + return nil, fmt.Errorf("failed to find network devices at %q", netDir) + } + + // Grab the first device from eg /sys/devices/pci0000:00/0000:00:19.0/net + for _, file := range files { + // Make sure it's really an interface + for _, l := range links { + if file.Name() == l.Attrs().Name { + return l, nil + } + } + } + } + + return nil, fmt.Errorf("failed to find physical interface") +} + func main() { skel.PluginMain(cmdAdd, cmdDel, version.All) } From ca3f28fa9e1b3e3e1e9b05653811416baed11365 Mon Sep 17 00:00:00 2001 From: Tino Rusch Date: Thu, 29 Jun 2017 11:44:59 +0200 Subject: [PATCH 3/3] host-device: cleanup + completed tests; --- plugins/host-device/host-device_test.go | 82 ++++++++++++++++++------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/plugins/host-device/host-device_test.go b/plugins/host-device/host-device_test.go index 0f38a094..68cfe4be 100644 --- a/plugins/host-device/host-device_test.go +++ b/plugins/host-device/host-device_test.go @@ -15,62 +15,102 @@ 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" + "github.com/vishvananda/netlink" ) +var ifname = "dummy0" + var _ = Describe("base functionality", func() { - var targetNs ns.NetNS + var originalNS ns.NetNS BeforeEach(func() { var err error - targetNs, err = ns.NewNS() + originalNS, err = ns.NewNS() Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { - targetNs.Close() + originalNS.Close() }) It("Works with a valid config", func() { - ifname := "eth0" + + // 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()) + link, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetUp(link) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdAdd + targetNS, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + conf := `{ - "name": "cni-plugin-host-device-test", - "type": "host-device", - "device": "eth0" -}` - conf = fmt.Sprintf(conf, ifname, targetNs.Path()) + "cniVersion": "0.3.0", + "name": "cni-plugin-host-device-test", + "type": "host-device", + "device": ifname + }` args := &skel.CmdArgs{ ContainerID: "dummy", - Netns: targetNs.Path(), + Netns: targetNS.Path(), IfName: ifname, StdinData: []byte(conf), } - _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) }) + _, _, err = testutils.CmdAddWithResult(targetNS.Path(), ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + // assert that dummy0 is now in the target namespace + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + link, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(ifname)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // 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()) }) It("fails an invalid config", func() { conf := `{ - "cniVersion": "0.3.0", - "name": "cni-plugin-sample-test", - "type": "host-device" -}` + "cniVersion": "0.3.0", + "name": "cni-plugin-host-device-test", + "type": "host-device" + }` args := &skel.CmdArgs{ ContainerID: "dummy", - Netns: targetNs.Path(), - IfName: "eth0", + Netns: originalNS.Path(), + IfName: ifname, StdinData: []byte(conf), } - _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) }) - Expect(err).To(MatchError("anotherAwesomeArg must be specified")) + _, _, err := testutils.CmdAddWithResult(originalNS.Path(), ifname, []byte(conf), func() error { return cmdAdd(args) }) + Expect(err).To(MatchError(`specify either "device", "hwaddr" or "kernelpath"`)) })