vlan: add VLAN plugin
authorDan Williams <dcbw@redhat.com>
Wed, 19 Apr 2017 03:06:54 +0000 (22:06 -0500)
committerDan Williams <dcbw@redhat.com>
Wed, 19 Apr 2017 18:41:12 +0000 (13:41 -0500)
plugins/vlan/vlan.go [new file with mode: 0644]
plugins/vlan/vlan_suite_test.go [new file with mode: 0644]
plugins/vlan/vlan_test.go [new file with mode: 0644]
test

diff --git a/plugins/vlan/vlan.go b/plugins/vlan/vlan.go
new file mode 100644 (file)
index 0000000..56735ee
--- /dev/null
@@ -0,0 +1,197 @@
+// Copyright 2015 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "encoding/json"
+       "errors"
+       "fmt"
+       "runtime"
+
+       "github.com/containernetworking/cni/pkg/ip"
+       "github.com/containernetworking/cni/pkg/ipam"
+       "github.com/containernetworking/cni/pkg/ns"
+       "github.com/containernetworking/cni/pkg/skel"
+       "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
+       "github.com/containernetworking/cni/pkg/version"
+       "github.com/vishvananda/netlink"
+)
+
+type NetConf struct {
+       types.NetConf
+       Master string `json:"master"`
+       VlanId int    `json:"vlanId"`
+       MTU    int    `json:"mtu,omitempty"`
+}
+
+func init() {
+       // this ensures that main runs only on main thread (thread group leader).
+       // since namespace ops (unshare, setns) are done for a single thread, we
+       // must ensure that the goroutine does not jump from OS thread to thread
+       runtime.LockOSThread()
+}
+
+func loadConf(bytes []byte) (*NetConf, string, error) {
+       n := &NetConf{}
+       if err := json.Unmarshal(bytes, n); err != nil {
+               return nil, "", fmt.Errorf("failed to load netconf: %v", err)
+       }
+       if n.Master == "" {
+               return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to create the VLAN for.`)
+       }
+       if n.VlanId < 0 || n.VlanId > 4094 {
+               return nil, "", fmt.Errorf(`invalid VLAN ID %d (must be between 0 and 4095 inclusive)`, n.VlanId)
+       }
+       return n, n.CNIVersion, nil
+}
+
+func createVlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
+       vlan := &current.Interface{}
+
+       m, err := netlink.LinkByName(conf.Master)
+       if err != nil {
+               return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
+       }
+
+       // due to kernel bug we have to create with tmpname or it might
+       // collide with the name on the host and error out
+       tmpName, err := ip.RandomVethName()
+       if err != nil {
+               return nil, err
+       }
+
+       if conf.MTU <= 0 {
+               conf.MTU = m.Attrs().MTU
+       }
+
+       v := &netlink.Vlan{
+               LinkAttrs: netlink.LinkAttrs{
+                       MTU:         conf.MTU,
+                       Name:        tmpName,
+                       ParentIndex: m.Attrs().Index,
+                       Namespace:   netlink.NsFd(int(netns.Fd())),
+               },
+               VlanId: conf.VlanId,
+       }
+
+       if err := netlink.LinkAdd(v); err != nil {
+               return nil, fmt.Errorf("failed to create vlan: %v", err)
+       }
+
+       err = netns.Do(func(_ ns.NetNS) error {
+               err := ip.RenameLink(tmpName, ifName)
+               if err != nil {
+                       return fmt.Errorf("failed to rename vlan to %q: %v", ifName, err)
+               }
+               vlan.Name = ifName
+
+               // Re-fetch interface to get all properties/attributes
+               contVlan, err := netlink.LinkByName(vlan.Name)
+               if err != nil {
+                       return fmt.Errorf("failed to refetch vlan %q: %v", vlan.Name, err)
+               }
+               vlan.Mac = contVlan.Attrs().HardwareAddr.String()
+               vlan.Sandbox = netns.Path()
+
+               return nil
+       })
+       if err != nil {
+               return nil, err
+       }
+
+       return vlan, nil
+}
+
+func cmdAdd(args *skel.CmdArgs) error {
+       n, cniVersion, err := loadConf(args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       netns, err := ns.GetNS(args.Netns)
+       if err != nil {
+               return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
+       }
+       defer netns.Close()
+
+       vlanInterface, err := createVlan(n, args.IfName, netns)
+       if err != nil {
+               return err
+       }
+
+       // run the IPAM plugin and get back the config to apply
+       r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+       // Convert whatever the IPAM result was into the current Result type
+       result, err := current.NewResultFromResult(r)
+       if err != nil {
+               return err
+       }
+
+       if len(result.IPs) == 0 {
+               return errors.New("IPAM plugin returned missing IP config")
+       }
+       for _, ipc := range result.IPs {
+               // All addresses belong to the vlan interface
+               ipc.Interface = 0
+       }
+
+       result.Interfaces = []*current.Interface{vlanInterface}
+
+       err = netns.Do(func(_ ns.NetNS) error {
+               return ipam.ConfigureIface(args.IfName, result)
+       })
+       if err != nil {
+               return err
+       }
+
+       result.DNS = n.DNS
+
+       return types.PrintResult(result, cniVersion)
+}
+
+func cmdDel(args *skel.CmdArgs) error {
+       n, _, err := loadConf(args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       err = ipam.ExecDel(n.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       if args.Netns == "" {
+               return nil
+       }
+
+       err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
+               _, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
+               // FIXME: use ip.ErrLinkNotFound when cni is revendored
+               if err != nil && err.Error() == "Link not found" {
+                       return nil
+               }
+               return err
+       })
+
+       return err
+}
+
+func main() {
+       skel.PluginMain(cmdAdd, cmdDel, version.All)
+}
diff --git a/plugins/vlan/vlan_suite_test.go b/plugins/vlan/vlan_suite_test.go
new file mode 100644 (file)
index 0000000..1445e57
--- /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 TestVlan(t *testing.T) {
+       RegisterFailHandler(Fail)
+       RunSpecs(t, "vlan Suite")
+}
diff --git a/plugins/vlan/vlan_test.go b/plugins/vlan/vlan_test.go
new file mode 100644 (file)
index 0000000..c46aa73
--- /dev/null
@@ -0,0 +1,237 @@
+// Copyright 2015 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "fmt"
+       "net"
+       "syscall"
+
+       "github.com/containernetworking/cni/pkg/ns"
+       "github.com/containernetworking/cni/pkg/skel"
+       "github.com/containernetworking/cni/pkg/testutils"
+       "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
+
+       "github.com/vishvananda/netlink"
+
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+const MASTER_NAME = "eth0"
+
+var _ = Describe("vlan 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())
+                       m, err := netlink.LinkByName(MASTER_NAME)
+                       Expect(err).NotTo(HaveOccurred())
+                       err = netlink.LinkSetUp(m)
+                       Expect(err).NotTo(HaveOccurred())
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+
+       AfterEach(func() {
+               Expect(originalNS.Close()).To(Succeed())
+       })
+
+       It("creates an vlan link in a non-default namespace with given MTU", func() {
+               conf := &NetConf{
+                       NetConf: types.NetConf{
+                               CNIVersion: "0.3.0",
+                               Name:       "testConfig",
+                               Type:       "vlan",
+                       },
+                       Master: MASTER_NAME,
+                       VlanId: 33,
+                       MTU:    1500,
+               }
+
+               // Create vlan in other namespace
+               targetNs, err := ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+               defer targetNs.Close()
+
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       _, err := createVlan(conf, "foobar0", targetNs)
+                       Expect(err).NotTo(HaveOccurred())
+                       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("foobar0")
+                       Expect(err).NotTo(HaveOccurred())
+                       Expect(link.Attrs().Name).To(Equal("foobar0"))
+                       Expect(link.Attrs().MTU).To(Equal(1500))
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+
+       It("creates an vlan link in a non-default namespace with master's MTU", func() {
+               conf := &NetConf{
+                       NetConf: types.NetConf{
+                               CNIVersion: "0.3.0",
+                               Name:       "testConfig",
+                               Type:       "vlan",
+                       },
+                       Master: MASTER_NAME,
+                       VlanId: 33,
+               }
+
+               // Create vlan in other namespace
+               targetNs, err := ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+               defer targetNs.Close()
+
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       m, err := netlink.LinkByName(MASTER_NAME)
+                       Expect(err).NotTo(HaveOccurred())
+                       err = netlink.LinkSetMTU(m, 1200)
+                       Expect(err).NotTo(HaveOccurred())
+
+                       _, err = createVlan(conf, "foobar0", targetNs)
+                       Expect(err).NotTo(HaveOccurred())
+                       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("foobar0")
+                       Expect(err).NotTo(HaveOccurred())
+                       Expect(link.Attrs().Name).To(Equal("foobar0"))
+                       Expect(link.Attrs().MTU).To(Equal(1200))
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+       })
+
+       It("configures and deconfigures an vlan link with ADD/DEL", func() {
+               const IFNAME = "eth0"
+
+               conf := fmt.Sprintf(`{
+    "cniVersion": "0.3.0",
+    "name": "mynet",
+    "type": "vlan",
+    "master": "%s",
+    "ipam": {
+        "type": "host-local",
+        "subnet": "10.1.2.0/24"
+    }
+}`, MASTER_NAME)
+
+               targetNs, err := ns.NewNS()
+               Expect(err).NotTo(HaveOccurred())
+               defer targetNs.Close()
+
+               args := &skel.CmdArgs{
+                       ContainerID: "dummy",
+                       Netns:       targetNs.Path(),
+                       IfName:      IFNAME,
+                       StdinData:   []byte(conf),
+               }
+
+               var result *current.Result
+               err = originalNS.Do(func(ns.NetNS) error {
+                       defer GinkgoRecover()
+
+                       r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
+                               return cmdAdd(args)
+                       })
+                       Expect(err).NotTo(HaveOccurred())
+
+                       result, err = current.GetResult(r)
+                       Expect(err).NotTo(HaveOccurred())
+
+                       Expect(len(result.Interfaces)).To(Equal(1))
+                       Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
+                       Expect(len(result.IPs)).To(Equal(1))
+                       return nil
+               })
+               Expect(err).NotTo(HaveOccurred())
+
+               // Make sure 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())
+
+               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 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/test b/test
index 83b5cda..342d4c6 100755 (executable)
--- a/test
+++ b/test
@@ -27,7 +27,7 @@ CNI_PATH=$(pwd)/bin:${CNI_PATH}
 
 echo "Running tests"
 
-TESTABLE="plugins/sample"
+TESTABLE="plugins/sample plugins/vlan"
 
 # user has not provided PKG override
 if [ -z "$PKG" ]; then