New vz-macvlan plugin
authorVZ Cambria <vzcambria@gmail.com>
Wed, 28 Dec 2016 16:17:33 +0000 (11:17 -0500)
committerVZ Cambria <vzcambria@gmail.com>
Wed, 28 Dec 2016 16:17:33 +0000 (11:17 -0500)
plugins/main/vz-macvlan/macvlan.go [new file with mode: 0644]
plugins/main/vz-macvlan/macvlan_suite_test.go [new file with mode: 0644]
plugins/main/vz-macvlan/macvlan_test.go [new file with mode: 0644]

diff --git a/plugins/main/vz-macvlan/macvlan.go b/plugins/main/vz-macvlan/macvlan.go
new file mode 100644 (file)
index 0000000..ef01269
--- /dev/null
@@ -0,0 +1,198 @@
+// Copyright 2015 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       "runtime"
+
+       "github.com/containernetworking/cni/pkg/ip"
+       "github.com/containernetworking/cni/pkg/ipam"
+       "github.com/containernetworking/cni/pkg/ns"
+       "github.com/containernetworking/cni/pkg/skel"
+       "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/utils/sysctl"
+       "github.com/containernetworking/cni/pkg/version"
+       "github.com/vishvananda/netlink"
+)
+
+const (
+       IPv4InterfaceArpProxySysctlTemplate = "net.ipv4.conf.%s.proxy_arp"
+)
+
+type NetConf struct {
+       types.NetConf
+       Master string `json:"master"`
+       Mode   string `json:"mode"`
+       MTU    int    `json:"mtu"`
+}
+
+func init() {
+       // this ensures that main runs only on main thread (thread group leader).
+       // since namespace ops (unshare, setns) are done for a single thread, we
+       // must ensure that the goroutine does not jump from OS thread to thread
+       runtime.LockOSThread()
+}
+
+func loadConf(bytes []byte) (*NetConf, error) {
+       n := &NetConf{}
+       if err := json.Unmarshal(bytes, n); err != nil {
+               return nil, fmt.Errorf("failed to load netconf: %v", err)
+       }
+       if n.Master == "" {
+               return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
+       }
+       return n, nil
+}
+
+func modeFromString(s string) (netlink.MacvlanMode, error) {
+       switch s {
+       case "", "bridge":
+               return netlink.MACVLAN_MODE_BRIDGE, nil
+       case "private":
+               return netlink.MACVLAN_MODE_PRIVATE, nil
+       case "vepa":
+               return netlink.MACVLAN_MODE_VEPA, nil
+       case "passthru":
+               return netlink.MACVLAN_MODE_PASSTHRU, nil
+       default:
+               return 0, fmt.Errorf("unknown macvlan mode: %q", s)
+       }
+}
+
+func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
+       mode, err := modeFromString(conf.Mode)
+       if err != nil {
+               return err
+       }
+
+       m, err := netlink.LinkByName(conf.Master)
+       if err != nil {
+               return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
+       }
+
+       // due to kernel bug we have to create with tmpName or it might
+       // collide with the name on the host and error out
+       tmpName, err := ip.RandomVethName()
+       if err != nil {
+               return err
+       }
+
+       mv := &netlink.Macvlan{
+               LinkAttrs: netlink.LinkAttrs{
+                       MTU:         conf.MTU,
+                       Name:        tmpName,
+                       ParentIndex: m.Attrs().Index,
+                       Namespace:   netlink.NsFd(int(netns.Fd())),
+               },
+               Mode: mode,
+       }
+
+       if err := netlink.LinkAdd(mv); err != nil {
+               return fmt.Errorf("failed to create macvlan: %v", err)
+       }
+
+       return netns.Do(func(_ ns.NetNS) error {
+               // TODO: duplicate following lines for ipv6 support, when it will be added in other places
+               ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
+               if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
+                       // remove the newly added link and ignore errors, because we already are in a failed state
+                       _ = netlink.LinkDel(mv)
+                       return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err)
+               }
+
+               err := renameLink(tmpName, ifName)
+               if err != nil {
+                       _ = netlink.LinkDel(mv)
+                       return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
+               }
+               return nil
+       })
+}
+
+func cmdAdd(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", netns, err)
+       }
+       defer netns.Close()
+
+       if err = createMacvlan(n, args.IfName, netns); err != nil {
+               return err
+       }
+
+       // run the IPAM plugin and get back the config to apply
+       result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+       if result.IP4 == nil {
+               return errors.New("IPAM plugin returned missing IPv4 config")
+       }
+
+       err = netns.Do(func(_ ns.NetNS) error {
+               if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil {
+                       return err
+               }
+
+               return ipam.ConfigureIface(args.IfName, result)
+       })
+       if err != nil {
+               return err
+       }
+
+       result.DNS = n.DNS
+       return result.Print()
+}
+
+func cmdDel(args *skel.CmdArgs) error {
+       n, err := loadConf(args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       if args.Netns == "" {
+               return nil
+       }
+
+       return ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
+               return ip.DelLinkByName(args.IfName)
+       })
+}
+
+func renameLink(curName, newName string) error {
+       link, err := netlink.LinkByName(curName)
+       if err != nil {
+               return err
+       }
+
+       return netlink.LinkSetName(link, newName)
+}
+
+func main() {
+       skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
+}
diff --git a/plugins/main/vz-macvlan/macvlan_suite_test.go b/plugins/main/vz-macvlan/macvlan_suite_test.go
new file mode 100644 (file)
index 0000000..844e567
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright 2016 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+
+       "testing"
+)
+
+func TestMacvlan(t *testing.T) {
+       RegisterFailHandler(Fail)
+       RunSpecs(t, "macvlan Suite")
+}
diff --git a/plugins/main/vz-macvlan/macvlan_test.go b/plugins/main/vz-macvlan/macvlan_test.go
new file mode 100644 (file)
index 0000000..9594a59
--- /dev/null
@@ -0,0 +1,175 @@
+// Copyright 2015 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "fmt"
+
+       "github.com/containernetworking/cni/pkg/ns"
+       "github.com/containernetworking/cni/pkg/skel"
+       "github.com/containernetworking/cni/pkg/testutils"
+       "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/utils/hwaddr"
+
+       "github.com/vishvananda/netlink"
+
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+const MASTER_NAME = "eth0"
+
+var _ = Describe("macvlan Operations", func() {
+       var originalNS ns.NetNS
+
+       BeforeEach(func() {
+               // Create a new NetNS so we don't modify the host
+               var err error
+               originalNS, err = ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       // Add master
+                       err = netlink.LinkAdd(&netlink.Dummy{
+                               LinkAttrs: netlink.LinkAttrs{
+                                       Name: MASTER_NAME,
+                               },
+                       })
+                       Expect(err).NotTo(HaveOccurred())
+                       _, err = netlink.LinkByName(MASTER_NAME)
+                       Expect(err).NotTo(HaveOccurred())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+
+       AfterEach(func() {
+               Expect(originalNS.Close()).To(Succeed())
+       })
+
+       It("creates an macvlan link in a non-default namespace", func() {
+               conf := &NetConf{
+                       NetConf: types.NetConf{
+                               CNIVersion: "0.2.0",
+                               Name:       "testConfig",
+                               Type:       "macvlan",
+                       },
+                       Master: MASTER_NAME,
+                       Mode:   "bridge",
+                       MTU:    1500,
+               }
+
+               targetNs, err := ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+               defer targetNs.Close()
+
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       err = createMacvlan(conf, "foobar0", targetNs)
+                       Expect(err).NotTo(HaveOccurred())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+
+               // Make sure macvlan link exists in the target namespace
+               err = targetNs.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       link, err := netlink.LinkByName("foobar0")
+                       Expect(err).NotTo(HaveOccurred())
+                       Expect(link.Attrs().Name).To(Equal("foobar0"))
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+
+       It("configures and deconfigures a macvlan link with ADD/DEL", func() {
+               const IFNAME = "macvl0"
+
+               conf := fmt.Sprintf(`{
+    "cniVersion": "0.2.0",
+    "name": "mynet",
+    "type": "macvlan",
+    "master": "%s",
+    "ipam": {
+        "type": "host-local",
+        "subnet": "10.1.2.0/24"
+    }
+}`, MASTER_NAME)
+
+               targetNs, err := ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+               defer targetNs.Close()
+
+               args := &skel.CmdArgs{
+                       ContainerID: "dummy",
+                       Netns:       targetNs.Path(),
+                       IfName:      IFNAME,
+                       StdinData:   []byte(conf),
+               }
+
+               // Make sure macvlan link exists in the target namespace
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error {
+                               return cmdAdd(args)
+                       })
+                       Expect(err).NotTo(HaveOccurred())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+
+               // Make sure macvlan link exists in the target namespace
+               err = targetNs.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       link, err := netlink.LinkByName(IFNAME)
+                       Expect(err).NotTo(HaveOccurred())
+                       Expect(link.Attrs().Name).To(Equal(IFNAME))
+
+                       hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
+                       Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
+
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
+                               return cmdDel(args)
+                       })
+                       Expect(err).NotTo(HaveOccurred())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+
+               // Make sure macvlan link has been deleted
+               err = targetNs.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       link, err := netlink.LinkByName(IFNAME)
+                       Expect(err).To(HaveOccurred())
+                       Expect(link).To(BeNil())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+})