diff --git a/plugins/vlan/vlan.go b/plugins/vlan/vlan.go new file mode 100644 index 00000000..56735ee1 --- /dev/null +++ b/plugins/vlan/vlan.go @@ -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 := ¤t.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 index 00000000..1445e573 --- /dev/null +++ b/plugins/vlan/vlan_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, "vlan Suite") +} diff --git a/plugins/vlan/vlan_test.go b/plugins/vlan/vlan_test.go new file mode 100644 index 00000000..c46aa739 --- /dev/null +++ b/plugins/vlan/vlan_test.go @@ -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 83b5cda6..342d4c6f 100755 --- 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