diff --git a/Makefile b/Makefile index 096d7fcc21..ec40b694f2 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ build: ${build_image}.created build-local: @mkdir -p "bin" - $(shell which godep) go build -o "bin/dnet" ./cmd/dnet + $(shell which godep) go build -tags experimental -o "bin/dnet" ./cmd/dnet clean: @if [ -d bin ]; then \ diff --git a/drivers/ipvlan/ipvlan.go b/drivers/ipvlan/ipvlan.go new file mode 100644 index 0000000000..047c531ee8 --- /dev/null +++ b/drivers/ipvlan/ipvlan.go @@ -0,0 +1,83 @@ +package ipvlan + +import ( + "net" + "sync" + + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/discoverapi" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/osl" +) + +const ( + vethLen = 7 + containerVethPrefix = "eth" + vethPrefix = "veth" + ipvlanType = "ipvlan" // driver type name + modeL2 = "l2" // ipvlan mode l2 is the default + modeL3 = "l3" // ipvlan L3 mode + parentOpt = "parent" // parent interface -o parent + modeOpt = "_mode" // ipvlan mode ux opt suffix +) + +var driverModeOpt = ipvlanType + modeOpt // mode -o ipvlan_mode + +type endpointTable map[string]*endpoint + +type networkTable map[string]*network + +type driver struct { + networks networkTable + sync.Once + sync.Mutex + store datastore.DataStore +} + +type endpoint struct { + id string + mac net.HardwareAddr + addr *net.IPNet + addrv6 *net.IPNet + srcName string +} + +type network struct { + id string + sbox osl.Sandbox + endpoints endpointTable + driver *driver + config *configuration + sync.Mutex +} + +// Init initializes and registers the libnetwork ipvlan driver +func Init(dc driverapi.DriverCallback, config map[string]interface{}) error { + c := driverapi.Capability{ + DataScope: datastore.LocalScope, + } + d := &driver{ + networks: networkTable{}, + } + d.initStore(config) + + return dc.RegisterDriver(ipvlanType, d, c) +} + +func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, error) { + return make(map[string]interface{}, 0), nil +} + +func (d *driver) Type() string { + return ipvlanType +} + +// DiscoverNew is a notification for a new discovery event. +func (d *driver) DiscoverNew(dType discoverapi.DiscoveryType, data interface{}) error { + return nil +} + +// DiscoverDelete is a notification for a discovery delete event. +func (d *driver) DiscoverDelete(dType discoverapi.DiscoveryType, data interface{}) error { + return nil +} diff --git a/drivers/ipvlan/ipvlan_endpoint.go b/drivers/ipvlan/ipvlan_endpoint.go new file mode 100644 index 0000000000..e3d724a3e5 --- /dev/null +++ b/drivers/ipvlan/ipvlan_endpoint.go @@ -0,0 +1,82 @@ +package ipvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/types" + "github.com/vishvananda/netlink" +) + +// CreateEndpoint assigns the mac, ip and endpoint id for the new container +func (d *driver) CreateEndpoint(nid, eid string, ifInfo driverapi.InterfaceInfo, + epOptions map[string]interface{}) error { + + if err := validateID(nid, eid); err != nil { + return err + } + n, err := d.getNetwork(nid) + if err != nil { + return fmt.Errorf("network id %q not found", nid) + } + if ifInfo.MacAddress() != nil { + return fmt.Errorf("%s interfaces do not support custom mac address assigment", ipvlanType) + } + ep := &endpoint{ + id: eid, + addr: ifInfo.Address(), + addrv6: ifInfo.AddressIPv6(), + mac: ifInfo.MacAddress(), + } + if ep.addr == nil { + return fmt.Errorf("create endpoint was not passed an IP address") + } + if ep.mac == nil { + ep.mac = netutils.GenerateMACFromIP(ep.addr.IP) + if err := ifInfo.SetMacAddress(ep.mac); err != nil { + return err + } + } + // disallow port mapping -p + if opt, ok := epOptions[netlabel.PortMap]; ok { + if _, ok := opt.([]types.PortBinding); ok { + if len(opt.([]types.PortBinding)) > 0 { + logrus.Warnf("%s driver does not support port mappings", ipvlanType) + } + } + } + // disallow port exposure --expose + if opt, ok := epOptions[netlabel.ExposedPorts]; ok { + if _, ok := opt.([]types.TransportPort); ok { + if len(opt.([]types.TransportPort)) > 0 { + logrus.Warnf("%s driver does not support port exposures", ipvlanType) + } + } + } + n.addEndpoint(ep) + + return nil +} + +// DeleteEndpoint remove the endpoint and associated netlink interface +func (d *driver) DeleteEndpoint(nid, eid string) error { + if err := validateID(nid, eid); err != nil { + return err + } + n := d.network(nid) + if n == nil { + return fmt.Errorf("network id %q not found", nid) + } + ep := n.endpoint(eid) + if ep == nil { + return fmt.Errorf("endpoint id %q not found", eid) + } + if link, err := netlink.LinkByName(ep.srcName); err == nil { + netlink.LinkDel(link) + } + + return nil +} diff --git a/drivers/ipvlan/ipvlan_joinleave.go b/drivers/ipvlan/ipvlan_joinleave.go new file mode 100644 index 0000000000..ee24d37bc6 --- /dev/null +++ b/drivers/ipvlan/ipvlan_joinleave.go @@ -0,0 +1,194 @@ +package ipvlan + +import ( + "fmt" + "net" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/osl" + "github.com/docker/libnetwork/types" +) + +type staticRoute struct { + Destination *net.IPNet + RouteType int + NextHop net.IP +} + +const ( + defaultV4RouteCidr = "0.0.0.0/0" + defaultV6RouteCidr = "::/0" +) + +// Join method is invoked when a Sandbox is attached to an endpoint. +func (d *driver) Join(nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error { + defer osl.InitOSContext()() + n, err := d.getNetwork(nid) + if err != nil { + return err + } + endpoint := n.endpoint(eid) + if endpoint == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + // generate a name for the iface that will be renamed to eth0 in the sbox + containerIfName, err := netutils.GenerateIfaceName(vethPrefix, vethLen) + if err != nil { + return fmt.Errorf("error generating an interface name: %v", err) + } + // create the netlink ipvlan interface + vethName, err := createIPVlan(containerIfName, n.config.Parent, n.config.IpvlanMode) + if err != nil { + return err + } + // bind the generated iface name to the endpoint + endpoint.srcName = vethName + ep := n.endpoint(eid) + if ep == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + if n.config.IpvlanMode == modeL3 { + // disable gateway services to add a default gw using dev eth0 only + jinfo.DisableGatewayService() + defaultRoute, err := ifaceGateway(defaultV4RouteCidr) + if err != nil { + return err + } + if err := jinfo.AddStaticRoute(defaultRoute.Destination, defaultRoute.RouteType, defaultRoute.NextHop); err != nil { + return fmt.Errorf("failed to set an ipvlan l3 mode ipv4 default gateway: %v", err) + } + logrus.Debugf("Ipvlan Endpoint Joined with IPv4_Addr: %s, Ipvlan_Mode: %s, Parent: %s", + ep.addr.IP.String(), n.config.IpvlanMode, n.config.Parent) + // If the endpoint has a v6 address, set a v6 default route + if ep.addrv6 != nil { + default6Route, err := ifaceGateway(defaultV6RouteCidr) + if err != nil { + return err + } + if err = jinfo.AddStaticRoute(default6Route.Destination, default6Route.RouteType, default6Route.NextHop); err != nil { + return fmt.Errorf("failed to set an ipvlan l3 mode ipv6 default gateway: %v", err) + } + logrus.Debugf("Ipvlan Endpoint Joined with IPv6_Addr: %s, Ipvlan_Mode: %s, Parent: %s", + ep.addrv6.IP.String(), n.config.IpvlanMode, n.config.Parent) + } + } + if n.config.IpvlanMode == modeL2 { + // parse and correlate the endpoint v4 address with the available v4 subnets + if len(n.config.Ipv4Subnets) > 0 { + s := n.getSubnetforIPv4(ep.addr) + if s == nil { + return fmt.Errorf("could not find a valid ipv4 subnet for endpoint %s", eid) + } + v4gw, _, err := net.ParseCIDR(s.GwIP) + if err != nil { + return fmt.Errorf("gatway %s is not a valid ipv4 address: %v", s.GwIP, err) + } + err = jinfo.SetGateway(v4gw) + if err != nil { + return err + } + logrus.Debugf("Ipvlan Endpoint Joined with IPv4_Addr: %s, Gateway: %s, Ipvlan_Mode: %s, Parent: %s", + ep.addr.IP.String(), v4gw.String(), n.config.IpvlanMode, n.config.Parent) + } + // parse and correlate the endpoint v6 address with the available v6 subnets + if len(n.config.Ipv6Subnets) > 0 { + s := n.getSubnetforIPv6(ep.addrv6) + if s == nil { + return fmt.Errorf("could not find a valid ipv6 subnet for endpoint %s", eid) + } + v6gw, _, err := net.ParseCIDR(s.GwIP) + if err != nil { + return fmt.Errorf("gatway %s is not a valid ipv6 address: %v", s.GwIP, err) + } + err = jinfo.SetGatewayIPv6(v6gw) + if err != nil { + return err + } + logrus.Debugf("Ipvlan Endpoint Joined with IPv6_Addr: %s, Gateway: %s, Ipvlan_Mode: %s, Parent: %s", + ep.addrv6.IP.String(), v6gw.String(), n.config.IpvlanMode, n.config.Parent) + } + } + iNames := jinfo.InterfaceName() + err = iNames.SetNames(vethName, containerVethPrefix) + if err != nil { + return err + } + + return nil +} + +// Leave method is invoked when a Sandbox detaches from an endpoint. +func (d *driver) Leave(nid, eid string) error { + network, err := d.getNetwork(nid) + if err != nil { + return err + } + endpoint, err := network.getEndpoint(eid) + if err != nil { + return err + } + if endpoint == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + + return nil +} + +// ifaceGateway returns a static route for either v4/v6 to be set to the container eth0 +func ifaceGateway(dfNet string) (*staticRoute, error) { + nh, dst, err := net.ParseCIDR(dfNet) + if err != nil { + return nil, fmt.Errorf("unable to parse default route %v", err) + } + defaultRoute := &staticRoute{ + Destination: dst, + RouteType: types.CONNECTED, + NextHop: nh, + } + + return defaultRoute, nil +} + +// getSubnetforIPv4 returns the ipv4 subnet to which the given IP belongs +func (n *network) getSubnetforIPv4(ip *net.IPNet) *ipv4Subnet { + for _, s := range n.config.Ipv4Subnets { + _, snet, err := net.ParseCIDR(s.SubnetIP) + if err != nil { + return nil + } + // first check if the mask lengths are the same + i, _ := snet.Mask.Size() + j, _ := ip.Mask.Size() + if i != j { + continue + } + if snet.Contains(ip.IP) { + return s + } + } + + return nil +} + +// getSubnetforIPv6 returns the ipv6 subnet to which the given IP belongs +func (n *network) getSubnetforIPv6(ip *net.IPNet) *ipv6Subnet { + for _, s := range n.config.Ipv6Subnets { + _, snet, err := net.ParseCIDR(s.SubnetIP) + if err != nil { + return nil + } + // first check if the mask lengths are the same + i, _ := snet.Mask.Size() + j, _ := ip.Mask.Size() + if i != j { + continue + } + if snet.Contains(ip.IP) { + return s + } + } + + return nil +} diff --git a/drivers/ipvlan/ipvlan_network.go b/drivers/ipvlan/ipvlan_network.go new file mode 100644 index 0000000000..c0fa276190 --- /dev/null +++ b/drivers/ipvlan/ipvlan_network.go @@ -0,0 +1,227 @@ +package ipvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/types" +) + +// CreateNetwork the network for the specified driver type +func (d *driver) CreateNetwork(nid string, option map[string]interface{}, ipV4Data, ipV6Data []driverapi.IPAMData) error { + // parse and validate the config and bind to networkConfiguration + config, err := parseNetworkOptions(nid, option) + if err != nil { + return err + } + config.ID = nid + err = config.processIPAM(nid, ipV4Data, ipV6Data) + if err != nil { + return err + } + // verify the ipvlan mode from -o ipvlan_mode option + switch config.IpvlanMode { + case "", modeL2: + // default to ipvlan L2 mode if -o ipvlan_mode is empty + config.IpvlanMode = modeL2 + case modeL3: + config.IpvlanMode = modeL3 + default: + return fmt.Errorf("requested ipvlan mode '%s' is not valid, 'l2' mode is the ipvlan driver default", config.IpvlanMode) + } + // loopback is not a valid parent link + if config.Parent == "lo" { + return fmt.Errorf("loopback interface is not a valid %s parent link", ipvlanType) + } + // if parent interface not specified, create a dummy type link to use named dummy+net_id + if config.Parent == "" { + config.Parent = getDummyName(stringid.TruncateID(config.ID)) + // empty parent and --internal are handled the same. Set here to update k/v + config.Internal = true + } + err = d.createNetwork(config) + if err != nil { + return err + } + // update persistent db, rollback on fail + err = d.storeUpdate(config) + if err != nil { + d.deleteNetwork(config.ID) + logrus.Debugf("encoutered an error rolling back a network create for %s : %v", config.ID, err) + return err + } + + return nil +} + +// createNetwork is used by new network callbacks and persistent network cache +func (d *driver) createNetwork(config *configuration) error { + // fail the network create if the ipvlan kernel module is unavailable + if err := kernelSupport(ipvlanType); err != nil { + return err + } + networkList := d.getNetworks() + for _, nw := range networkList { + if config.Parent == nw.config.Parent { + return fmt.Errorf("network %s is already using parent interface %s", + getDummyName(stringid.TruncateID(nw.config.ID)), config.Parent) + } + } + if !parentExists(config.Parent) { + // if the --internal flag is set, create a dummy link + if config.Internal { + err := createDummyLink(config.Parent, getDummyName(stringid.TruncateID(config.ID))) + if err != nil { + return err + } + config.CreatedSlaveLink = true + // notify the user in logs they have limited comunicatins + if config.Parent == getDummyName(stringid.TruncateID(config.ID)) { + logrus.Debugf("Empty -o parent= and --internal flags limit communications to other containers inside of network: %s", + config.Parent) + } + } else { + // if the subinterface parent_iface.vlan_id checks do not pass, return err. + // a valid example is 'eth0.10' for a parent iface 'eth0' with a vlan id '10' + err := createVlanLink(config.Parent) + if err != nil { + return err + } + // if driver created the networks slave link, record it for future deletion + config.CreatedSlaveLink = true + } + } + n := &network{ + id: config.ID, + driver: d, + endpoints: endpointTable{}, + config: config, + } + // add the *network + d.addNetwork(n) + + return nil +} + +// DeleteNetwork the network for the specified driver type +func (d *driver) DeleteNetwork(nid string) error { + n := d.network(nid) + if n == nil { + return fmt.Errorf("network id %s not found", nid) + } + // if the driver created the slave interface, delete it, otherwise leave it + if ok := n.config.CreatedSlaveLink; ok { + // if the interface exists, only delete if it matches iface.vlan or dummy.net_id naming + if ok := parentExists(n.config.Parent); ok { + // only delete the link if it is named the net_id + if n.config.Parent == getDummyName(stringid.TruncateID(nid)) { + err := delDummyLink(n.config.Parent) + if err != nil { + logrus.Debugf("link %s was not deleted, continuing the delete network operation: %v", + n.config.Parent, err) + } + } else { + // only delete the link if it matches iface.vlan naming + err := delVlanLink(n.config.Parent) + if err != nil { + logrus.Debugf("link %s was not deleted, continuing the delete network operation: %v", + n.config.Parent, err) + } + } + } + } + // delete the *network + d.deleteNetwork(nid) + // delete the network record from persistent cache + err := d.storeDelete(n.config) + if err != nil { + return fmt.Errorf("error deleting deleting id %s from datastore: %v", nid, err) + } + return nil +} + +// parseNetworkOptions parse docker network options +func parseNetworkOptions(id string, option options.Generic) (*configuration, error) { + var ( + err error + config = &configuration{} + ) + // parse generic labels first + if genData, ok := option[netlabel.GenericData]; ok && genData != nil { + if config, err = parseNetworkGenericOptions(genData); err != nil { + return nil, err + } + } + // setting the parent to "" will trigger an isolated network dummy parent link + if _, ok := option[netlabel.Internal]; ok { + config.Internal = true + // empty --parent= and --internal are handled the same. + config.Parent = "" + } + return config, nil +} + +// parseNetworkGenericOptions parse generic driver docker network options +func parseNetworkGenericOptions(data interface{}) (*configuration, error) { + var ( + err error + config *configuration + ) + switch opt := data.(type) { + case *configuration: + config = opt + case map[string]string: + config = &configuration{} + err = config.fromOptions(opt) + case options.Generic: + var opaqueConfig interface{} + if opaqueConfig, err = options.GenerateFromModel(opt, config); err == nil { + config = opaqueConfig.(*configuration) + } + default: + err = types.BadRequestErrorf("unrecognized network configuration format: %v", opt) + } + return config, err +} + +// fromOptions binds the generic options to networkConfiguration to cache +func (config *configuration) fromOptions(labels map[string]string) error { + for label, value := range labels { + switch label { + case parentOpt: + // parse driver option '-o parent' + config.Parent = value + case driverModeOpt: + // parse driver option '-o ipvlan_mode' + config.IpvlanMode = value + } + } + return nil +} + +// processIPAM parses v4 and v6 IP information and binds it to the network configuration +func (config *configuration) processIPAM(id string, ipamV4Data, ipamV6Data []driverapi.IPAMData) error { + if len(ipamV4Data) > 0 { + for _, ipd := range ipamV4Data { + s := &ipv4Subnet{ + SubnetIP: ipd.Pool.String(), + GwIP: ipd.Gateway.String(), + } + config.Ipv4Subnets = append(config.Ipv4Subnets, s) + } + } + if len(ipamV6Data) > 0 { + for _, ipd := range ipamV6Data { + s := &ipv6Subnet{ + SubnetIP: ipd.Pool.String(), + GwIP: ipd.Gateway.String(), + } + config.Ipv6Subnets = append(config.Ipv6Subnets, s) + } + } + return nil +} diff --git a/drivers/ipvlan/ipvlan_setup.go b/drivers/ipvlan/ipvlan_setup.go new file mode 100644 index 0000000000..3f8f4c2757 --- /dev/null +++ b/drivers/ipvlan/ipvlan_setup.go @@ -0,0 +1,228 @@ +package ipvlan + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/osl" + "github.com/vishvananda/netlink" +) + +const ( + dummyPrefix = "di-" // ipvlan prefix for dummy parent interface + ipvlanKernelVer = "3.19" // minimum ipvlan kernel version support +) + +// createIPVlan Create the ipvlan slave specifying the source name +func createIPVlan(containerIfName, parent, ipvlanMode string) (string, error) { + defer osl.InitOSContext()() + + // Set the ipvlan mode. Default is bridge mode + mode, err := setIPVlanMode(ipvlanMode) + if err != nil { + return "", fmt.Errorf("Unsupported %s ipvlan mode: %v", ipvlanMode, err) + } + // verify the Docker host interface acting as the macvlan parent iface exists + if !parentExists(parent) { + return "", fmt.Errorf("the requested parent interface %s was not found on the Docker host", parent) + } + // Get the link for the master index (Example: the docker host eth iface) + parentLink, err := netlink.LinkByName(parent) + if err != nil { + return "", fmt.Errorf("error occoured looking up the %s parent iface %s error: %s", ipvlanType, parent, err) + } + // Create a ipvlan link + ipvlan := &netlink.IPVlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: containerIfName, + ParentIndex: parentLink.Attrs().Index, + }, + Mode: mode, + } + if err := netlink.LinkAdd(ipvlan); err != nil { + // If a user creates a macvlan and ipvlan on same parent, only one slave iface can be active at a time. + return "", fmt.Errorf("failed to create the %s port: %v", ipvlanType, err) + } + + return ipvlan.Attrs().Name, nil +} + +// setIPVlanMode setter for one of the two ipvlan port types +func setIPVlanMode(mode string) (netlink.IPVlanMode, error) { + switch mode { + case modeL2: + return netlink.IPVLAN_MODE_L2, nil + case modeL3: + return netlink.IPVLAN_MODE_L3, nil + default: + return 0, fmt.Errorf("Unknown ipvlan mode: %s", mode) + } +} + +// parentExists check if the specified interface exists in the default namespace +func parentExists(ifaceStr string) bool { + _, err := netlink.LinkByName(ifaceStr) + if err != nil { + return false + } + + return true +} + +// kernelSupport for the necessary kernel module for the driver type +func kernelSupport(networkTpe string) error { + // attempt to load the module, silent if successful or already loaded + exec.Command("modprobe", ipvlanType).Run() + f, err := os.Open("/proc/modules") + if err != nil { + return err + } + defer f.Close() + s := bufio.NewScanner(f) + for s.Scan() { + if strings.Contains(s.Text(), ipvlanType) { + return nil + } + } + + return fmt.Errorf("unable to load the Linux kernel module %s", ipvlanType) +} + +// createVlanLink parses sub-interfaces and vlan id for creation +func createVlanLink(parentName string) error { + if strings.Contains(parentName, ".") { + parent, vidInt, err := parseVlan(parentName) + if err != nil { + return err + } + // VLAN identifier or VID is a 12-bit field specifying the VLAN to which the frame belongs + if vidInt > 4094 || vidInt < 1 { + return fmt.Errorf("vlan id must be between 1-4094, received: %d", vidInt) + } + // get the parent link to attach a vlan subinterface + parentLink, err := netlink.LinkByName(parent) + if err != nil { + return fmt.Errorf("failed to find master interface %s on the Docker host: %v", parent, err) + } + vlanLink := &netlink.Vlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: parentName, + ParentIndex: parentLink.Attrs().Index, + }, + VlanId: vidInt, + } + // create the subinterface + if err := netlink.LinkAdd(vlanLink); err != nil { + return fmt.Errorf("failed to create %s vlan link: %v", vlanLink.Name, err) + } + // Bring the new netlink iface up + if err := netlink.LinkSetUp(vlanLink); err != nil { + return fmt.Errorf("failed to enable %s the ipvlan parent link %v", vlanLink.Name, err) + } + logrus.Debugf("Added a vlan tagged netlink subinterface: %s with a vlan id: %d", parentName, vidInt) + return nil + } + + return fmt.Errorf("invalid subinterface vlan name %s, example formatting is eth0.10", parentName) +} + +// delVlanLink verifies only sub-interfaces with a vlan id get deleted +func delVlanLink(linkName string) error { + if strings.Contains(linkName, ".") { + _, _, err := parseVlan(linkName) + if err != nil { + return err + } + // delete the vlan subinterface + vlanLink, err := netlink.LinkByName(linkName) + if err != nil { + return fmt.Errorf("failed to find interface %s on the Docker host : %v", linkName, err) + } + // verify a parent interface isn't being deleted + if vlanLink.Attrs().ParentIndex == 0 { + return fmt.Errorf("interface %s does not appear to be a slave device: %v", linkName, err) + } + // delete the ipvlan slave device + if err := netlink.LinkDel(vlanLink); err != nil { + return fmt.Errorf("failed to delete %s link: %v", linkName, err) + } + logrus.Debugf("Deleted a vlan tagged netlink subinterface: %s", linkName) + } + // if the subinterface doesn't parse to iface.vlan_id leave the interface in + // place since it could be a user specified name not created by the driver. + return nil +} + +// parseVlan parses and verifies a slave interface name: -o parent=eth0.10 +func parseVlan(linkName string) (string, int, error) { + // parse -o parent=eth0.10 + splitName := strings.Split(linkName, ".") + if len(splitName) != 2 { + return "", 0, fmt.Errorf("required interface name format is: name.vlan_id, ex. eth0.10 for vlan 10, instead received %s", linkName) + } + parent, vidStr := splitName[0], splitName[1] + // validate type and convert vlan id to int + vidInt, err := strconv.Atoi(vidStr) + if err != nil { + return "", 0, fmt.Errorf("unable to parse a valid vlan id from: %s (ex. eth0.10 for vlan 10)", vidStr) + } + // Check if the interface exists + if !parentExists(parent) { + return "", 0, fmt.Errorf("-o parent interface does was not found on the host: %s", parent) + } + + return parent, vidInt, nil +} + +// createDummyLink creates a dummy0 parent link +func createDummyLink(dummyName, truncNetID string) error { + // create a parent interface since one was not specified + parent := &netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: dummyName, + }, + } + if err := netlink.LinkAdd(parent); err != nil { + return err + } + parentDummyLink, err := netlink.LinkByName(dummyName) + if err != nil { + return fmt.Errorf("error occoured looking up the %s parent iface %s error: %s", ipvlanType, dummyName, err) + } + // bring the new netlink iface up + if err := netlink.LinkSetUp(parentDummyLink); err != nil { + return fmt.Errorf("failed to enable %s the ipvlan parent link: %v", dummyName, err) + } + + return nil +} + +// delDummyLink deletes the link type dummy used when -o parent is not passed +func delDummyLink(linkName string) error { + // delete the vlan subinterface + dummyLink, err := netlink.LinkByName(linkName) + if err != nil { + return fmt.Errorf("failed to find link %s on the Docker host : %v", linkName, err) + } + // verify a parent interface is being deleted + if dummyLink.Attrs().ParentIndex != 0 { + return fmt.Errorf("link %s is not a parent dummy interface", linkName) + } + // delete the ipvlan dummy device + if err := netlink.LinkDel(dummyLink); err != nil { + return fmt.Errorf("failed to delete the dummy %s link: %v", linkName, err) + } + logrus.Debugf("Deleted a dummy parent link: %s", linkName) + + return nil +} + +// getDummyName returns the name of a dummy parent with truncated net ID and driver prefix +func getDummyName(netID string) string { + return fmt.Sprintf("%s%s", dummyPrefix, netID) +} diff --git a/drivers/ipvlan/ipvlan_setup_test.go b/drivers/ipvlan/ipvlan_setup_test.go new file mode 100644 index 0000000000..098a6ecd73 --- /dev/null +++ b/drivers/ipvlan/ipvlan_setup_test.go @@ -0,0 +1,87 @@ +package ipvlan + +import ( + "testing" + + "github.com/vishvananda/netlink" +) + +// TestValidateLink tests the parentExists function +func TestValidateLink(t *testing.T) { + validIface := "lo" + invalidIface := "foo12345" + + // test a valid parent interface validation + if ok := parentExists(validIface); !ok { + t.Fatalf("failed validating loopback %s", validIface) + } + // test a invalid parent interface validation + if ok := parentExists(invalidIface); ok { + t.Fatalf("failed to invalidate interface %s", invalidIface) + } +} + +// TestValidateSubLink tests valid 802.1q naming convention +func TestValidateSubLink(t *testing.T) { + validSubIface := "lo.10" + invalidSubIface1 := "lo" + invalidSubIface2 := "lo:10" + invalidSubIface3 := "foo123.456" + + // test a valid parent_iface.vlan_id + _, _, err := parseVlan(validSubIface) + if err != nil { + t.Fatalf("failed subinterface validation: %v", err) + } + // test a invalid vid with a valid parent link + _, _, err = parseVlan(invalidSubIface1) + if err == nil { + t.Fatalf("failed subinterface validation test: %s", invalidSubIface1) + } + // test a valid vid with a valid parent link with a invalid delimiter + _, _, err = parseVlan(invalidSubIface2) + if err == nil { + t.Fatalf("failed subinterface validation test: %v", invalidSubIface2) + } + // test a invalid parent link with a valid vid + _, _, err = parseVlan(invalidSubIface3) + if err == nil { + t.Fatalf("failed subinterface validation test: %v", invalidSubIface3) + } +} + +// TestSetIPVlanMode tests the ipvlan mode setter +func TestSetIPVlanMode(t *testing.T) { + // test ipvlan l2 mode + mode, err := setIPVlanMode(modeL2) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.IPVLAN_MODE_L2 { + t.Fatalf("expected %d got %d", netlink.IPVLAN_MODE_L2, mode) + } + // test ipvlan l3 mode + mode, err = setIPVlanMode(modeL3) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.IPVLAN_MODE_L3 { + t.Fatalf("expected %d got %d", netlink.IPVLAN_MODE_L3, mode) + } + // test invalid mode + mode, err = setIPVlanMode("foo") + if err == nil { + t.Fatal("invalid ipvlan mode should have returned an error") + } + if mode != 0 { + t.Fatalf("expected 0 got %d", mode) + } + // test null mode + mode, err = setIPVlanMode("") + if err == nil { + t.Fatal("invalid ipvlan mode should have returned an error") + } + if mode != 0 { + t.Fatalf("expected 0 got %d", mode) + } +} diff --git a/drivers/ipvlan/ipvlan_state.go b/drivers/ipvlan/ipvlan_state.go new file mode 100644 index 0000000000..2d8cb2d8f6 --- /dev/null +++ b/drivers/ipvlan/ipvlan_state.go @@ -0,0 +1,115 @@ +package ipvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/osl" + "github.com/docker/libnetwork/types" +) + +func (d *driver) network(nid string) *network { + d.Lock() + n, ok := d.networks[nid] + d.Unlock() + if !ok { + logrus.Errorf("network id %s not found", nid) + } + + return n +} + +func (d *driver) addNetwork(n *network) { + d.Lock() + d.networks[n.id] = n + d.Unlock() +} + +func (d *driver) deleteNetwork(nid string) { + d.Lock() + delete(d.networks, nid) + d.Unlock() +} + +// getNetworks Safely returns a slice of existng networks +func (d *driver) getNetworks() []*network { + d.Lock() + defer d.Unlock() + + ls := make([]*network, 0, len(d.networks)) + for _, nw := range d.networks { + ls = append(ls, nw) + } + + return ls +} + +func (n *network) endpoint(eid string) *endpoint { + n.Lock() + defer n.Unlock() + + return n.endpoints[eid] +} + +func (n *network) addEndpoint(ep *endpoint) { + n.Lock() + n.endpoints[ep.id] = ep + n.Unlock() +} + +func (n *network) deleteEndpoint(eid string) { + n.Lock() + delete(n.endpoints, eid) + n.Unlock() +} + +func (n *network) getEndpoint(eid string) (*endpoint, error) { + n.Lock() + defer n.Unlock() + if eid == "" { + return nil, fmt.Errorf("endpoint id %s not found", eid) + } + if ep, ok := n.endpoints[eid]; ok { + return ep, nil + } + + return nil, nil +} + +func validateID(nid, eid string) error { + if nid == "" { + return fmt.Errorf("invalid network id") + } + if eid == "" { + return fmt.Errorf("invalid endpoint id") + } + + return nil +} + +func (n *network) sandbox() osl.Sandbox { + n.Lock() + defer n.Unlock() + + return n.sbox +} + +func (n *network) setSandbox(sbox osl.Sandbox) { + n.Lock() + n.sbox = sbox + n.Unlock() +} + +func (d *driver) getNetwork(id string) (*network, error) { + d.Lock() + defer d.Unlock() + if id == "" { + return nil, types.BadRequestErrorf("invalid network id: %s", id) + } + + if nw, ok := d.networks[id]; ok { + return nw, nil + } + + return nil, types.NotFoundErrorf("network not found: %s", id) +} diff --git a/drivers/ipvlan/ipvlan_store.go b/drivers/ipvlan/ipvlan_store.go new file mode 100644 index 0000000000..f6746da670 --- /dev/null +++ b/drivers/ipvlan/ipvlan_store.go @@ -0,0 +1,217 @@ +package ipvlan + +import ( + "encoding/json" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libkv/store/boltdb" + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/discoverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/types" +) + +const ipvlanPrefix = "ipvlan" // prefix used for persistent driver storage + +// networkConfiguration for this driver's network specific configuration +type configuration struct { + ID string + Mtu int + dbIndex uint64 + dbExists bool + Internal bool + Parent string + IpvlanMode string + CreatedSlaveLink bool + Ipv4Subnets []*ipv4Subnet + Ipv6Subnets []*ipv6Subnet +} + +type ipv4Subnet struct { + SubnetIP string + GwIP string +} + +type ipv6Subnet struct { + SubnetIP string + GwIP string +} + +// initStore drivers are responsible for caching their own persistent state +func (d *driver) initStore(option map[string]interface{}) error { + if data, ok := option[netlabel.LocalKVClient]; ok { + var err error + dsc, ok := data.(discoverapi.DatastoreConfigData) + if !ok { + return types.InternalErrorf("incorrect data in datastore configuration: %v", data) + } + d.store, err = datastore.NewDataStoreFromConfig(dsc) + if err != nil { + return types.InternalErrorf("ipvlan driver failed to initialize data store: %v", err) + } + + return d.populateNetworks() + } + + return nil +} + +// populateNetworks is invoked at driver init to recreate persistently stored networks +func (d *driver) populateNetworks() error { + kvol, err := d.store.List(datastore.Key(ipvlanPrefix), &configuration{}) + if err != nil && err != datastore.ErrKeyNotFound && err != boltdb.ErrBoltBucketNotFound { + return fmt.Errorf("failed to get ipvlan network configurations from store: %v", err) + } + // If empty it simply means no ipvlan networks have been created yet + if err == datastore.ErrKeyNotFound { + return nil + } + for _, kvo := range kvol { + config := kvo.(*configuration) + if err = d.createNetwork(config); err != nil { + logrus.Warnf("could not create ipvlan network for id %s from persistent state", config.ID) + } + } + + return nil +} + +// storeUpdate used to update persistent ipvlan network records as they are created +func (d *driver) storeUpdate(kvObject datastore.KVObject) error { + if d.store == nil { + logrus.Warnf("ipvlan store not initialized. kv object %s is not added to the store", datastore.Key(kvObject.Key()...)) + return nil + } + if err := d.store.PutObjectAtomic(kvObject); err != nil { + return fmt.Errorf("failed to update ipvlan store for object type %T: %v", kvObject, err) + } + + return nil +} + +// storeDelete used to delete ipvlan network records from persistent cache as they are deleted +func (d *driver) storeDelete(kvObject datastore.KVObject) error { + if d.store == nil { + logrus.Debugf("ipvlan store not initialized. kv object %s is not deleted from store", datastore.Key(kvObject.Key()...)) + return nil + } +retry: + if err := d.store.DeleteObjectAtomic(kvObject); err != nil { + if err == datastore.ErrKeyModified { + if err := d.store.GetObject(datastore.Key(kvObject.Key()...), kvObject); err != nil { + return fmt.Errorf("could not update the kvobject to latest when trying to delete: %v", err) + } + goto retry + } + return err + } + + return nil +} + +func (config *configuration) MarshalJSON() ([]byte, error) { + nMap := make(map[string]interface{}) + nMap["ID"] = config.ID + nMap["Mtu"] = config.Mtu + nMap["Parent"] = config.Parent + nMap["IpvlanMode"] = config.IpvlanMode + nMap["Internal"] = config.Internal + nMap["CreatedSubIface"] = config.CreatedSlaveLink + if len(config.Ipv4Subnets) > 0 { + iis, err := json.Marshal(config.Ipv4Subnets) + if err != nil { + return nil, err + } + nMap["Ipv4Subnets"] = string(iis) + } + if len(config.Ipv6Subnets) > 0 { + iis, err := json.Marshal(config.Ipv6Subnets) + if err != nil { + return nil, err + } + nMap["Ipv6Subnets"] = string(iis) + } + + return json.Marshal(nMap) +} + +func (config *configuration) UnmarshalJSON(b []byte) error { + var ( + err error + nMap map[string]interface{} + ) + + if err = json.Unmarshal(b, &nMap); err != nil { + return err + } + config.ID = nMap["ID"].(string) + config.Mtu = int(nMap["Mtu"].(float64)) + config.Parent = nMap["Parent"].(string) + config.IpvlanMode = nMap["IpvlanMode"].(string) + config.Internal = nMap["Internal"].(bool) + config.CreatedSlaveLink = nMap["CreatedSubIface"].(bool) + if v, ok := nMap["Ipv4Subnets"]; ok { + if err := json.Unmarshal([]byte(v.(string)), &config.Ipv4Subnets); err != nil { + return err + } + } + if v, ok := nMap["Ipv6Subnets"]; ok { + if err := json.Unmarshal([]byte(v.(string)), &config.Ipv6Subnets); err != nil { + return err + } + } + + return nil +} + +func (config *configuration) Key() []string { + return []string{ipvlanPrefix, config.ID} +} + +func (config *configuration) KeyPrefix() []string { + return []string{ipvlanPrefix} +} + +func (config *configuration) Value() []byte { + b, err := json.Marshal(config) + if err != nil { + return nil + } + return b +} + +func (config *configuration) SetValue(value []byte) error { + return json.Unmarshal(value, config) +} + +func (config *configuration) Index() uint64 { + return config.dbIndex +} + +func (config *configuration) SetIndex(index uint64) { + config.dbIndex = index + config.dbExists = true +} + +func (config *configuration) Exists() bool { + return config.dbExists +} + +func (config *configuration) Skip() bool { + return false +} + +func (config *configuration) New() datastore.KVObject { + return &configuration{} +} + +func (config *configuration) CopyTo(o datastore.KVObject) error { + dstNcfg := o.(*configuration) + *dstNcfg = *config + return nil +} + +func (config *configuration) DataScope() string { + return datastore.LocalScope +} diff --git a/drivers/ipvlan/ipvlan_test.go b/drivers/ipvlan/ipvlan_test.go new file mode 100644 index 0000000000..6d4ee218fc --- /dev/null +++ b/drivers/ipvlan/ipvlan_test.go @@ -0,0 +1,60 @@ +package ipvlan + +import ( + "testing" + + "github.com/docker/libnetwork/driverapi" + _ "github.com/docker/libnetwork/testutils" +) + +const testNetworkType = "ipvlan" + +type driverTester struct { + t *testing.T + d *driver +} + +func (dt *driverTester) RegisterDriver(name string, drv driverapi.Driver, + cap driverapi.Capability) error { + if name != testNetworkType { + dt.t.Fatalf("Expected driver register name to be %q. Instead got %q", + testNetworkType, name) + } + + if _, ok := drv.(*driver); !ok { + dt.t.Fatalf("Expected driver type to be %T. Instead got %T", + &driver{}, drv) + } + + dt.d = drv.(*driver) + return nil +} + +func TestIpvlanInit(t *testing.T) { + if err := Init(&driverTester{t: t}, nil); err != nil { + t.Fatal(err) + } +} + +func TestIpvlanNilConfig(t *testing.T) { + dt := &driverTester{t: t} + if err := Init(dt, nil); err != nil { + t.Fatal(err) + } + + if err := dt.d.initStore(nil); err != nil { + t.Fatal(err) + } +} + +func TestIpvlanType(t *testing.T) { + dt := &driverTester{t: t} + if err := Init(dt, nil); err != nil { + t.Fatal(err) + } + + if dt.d.Type() != testNetworkType { + t.Fatalf("Expected Type() to return %q. Instead got %q", testNetworkType, + dt.d.Type()) + } +} diff --git a/drivers/macvlan/macvlan.go b/drivers/macvlan/macvlan.go new file mode 100644 index 0000000000..502f166fd1 --- /dev/null +++ b/drivers/macvlan/macvlan.go @@ -0,0 +1,85 @@ +package macvlan + +import ( + "net" + "sync" + + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/discoverapi" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/osl" +) + +const ( + vethLen = 7 + containerVethPrefix = "eth" + vethPrefix = "veth" + macvlanType = "macvlan" // driver type name + modePrivate = "private" // macvlan mode private + modeVepa = "vepa" // macvlan mode vepa + modeBridge = "bridge" // macvlan mode bridge + modePassthru = "passthru" // macvlan mode passthrough + parentOpt = "parent" // parent interface -o parent + modeOpt = "_mode" // macvlan mode ux opt suffix +) + +var driverModeOpt = macvlanType + modeOpt // mode --option macvlan_mode + +type endpointTable map[string]*endpoint + +type networkTable map[string]*network + +type driver struct { + networks networkTable + sync.Once + sync.Mutex + store datastore.DataStore +} + +type endpoint struct { + id string + mac net.HardwareAddr + addr *net.IPNet + addrv6 *net.IPNet + srcName string +} + +type network struct { + id string + sbox osl.Sandbox + endpoints endpointTable + driver *driver + config *configuration + sync.Mutex +} + +// Init initializes and registers the libnetwork macvlan driver +func Init(dc driverapi.DriverCallback, config map[string]interface{}) error { + c := driverapi.Capability{ + DataScope: datastore.LocalScope, + } + d := &driver{ + networks: networkTable{}, + } + d.initStore(config) + + return dc.RegisterDriver(macvlanType, d, c) +} + +func (d *driver) EndpointOperInfo(nid, eid string) (map[string]interface{}, error) { + return make(map[string]interface{}, 0), nil +} + +func (d *driver) Type() string { + return macvlanType +} + +// DiscoverNew is a notification for a new discovery event +func (d *driver) DiscoverNew(dType discoverapi.DiscoveryType, data interface{}) error { + return nil +} + +// DiscoverDelete is a notification for a discovery delete event +func (d *driver) DiscoverDelete(dType discoverapi.DiscoveryType, data interface{}) error { + return nil +} diff --git a/drivers/macvlan/macvlan_endpoint.go b/drivers/macvlan/macvlan_endpoint.go new file mode 100644 index 0000000000..7f99092222 --- /dev/null +++ b/drivers/macvlan/macvlan_endpoint.go @@ -0,0 +1,79 @@ +package macvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/types" + "github.com/vishvananda/netlink" +) + +// CreateEndpoint assigns the mac, ip and endpoint id for the new container +func (d *driver) CreateEndpoint(nid, eid string, ifInfo driverapi.InterfaceInfo, + epOptions map[string]interface{}) error { + + if err := validateID(nid, eid); err != nil { + return err + } + n, err := d.getNetwork(nid) + if err != nil { + return fmt.Errorf("network id %q not found", nid) + } + ep := &endpoint{ + id: eid, + addr: ifInfo.Address(), + addrv6: ifInfo.AddressIPv6(), + mac: ifInfo.MacAddress(), + } + if ep.addr == nil { + return fmt.Errorf("create endpoint was not passed an IP address") + } + if ep.mac == nil { + ep.mac = netutils.GenerateMACFromIP(ep.addr.IP) + if err := ifInfo.SetMacAddress(ep.mac); err != nil { + return err + } + } + // disallow portmapping -p + if opt, ok := epOptions[netlabel.PortMap]; ok { + if _, ok := opt.([]types.PortBinding); ok { + if len(opt.([]types.PortBinding)) > 0 { + logrus.Warnf("%s driver does not support port mappings", macvlanType) + } + } + } + // disallow port exposure --expose + if opt, ok := epOptions[netlabel.ExposedPorts]; ok { + if _, ok := opt.([]types.TransportPort); ok { + if len(opt.([]types.TransportPort)) > 0 { + logrus.Warnf("%s driver does not support port exposures", macvlanType) + } + } + } + n.addEndpoint(ep) + + return nil +} + +// DeleteEndpoint remove the endpoint and associated netlink interface +func (d *driver) DeleteEndpoint(nid, eid string) error { + if err := validateID(nid, eid); err != nil { + return err + } + n := d.network(nid) + if n == nil { + return fmt.Errorf("network id %q not found", nid) + } + ep := n.endpoint(eid) + if ep == nil { + return fmt.Errorf("endpoint id %q not found", eid) + } + if link, err := netlink.LinkByName(ep.srcName); err == nil { + netlink.LinkDel(link) + } + + return nil +} diff --git a/drivers/macvlan/macvlan_joinleave.go b/drivers/macvlan/macvlan_joinleave.go new file mode 100644 index 0000000000..d7eeda81d1 --- /dev/null +++ b/drivers/macvlan/macvlan_joinleave.go @@ -0,0 +1,140 @@ +package macvlan + +import ( + "fmt" + "net" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/osl" +) + +// Join method is invoked when a Sandbox is attached to an endpoint. +func (d *driver) Join(nid, eid string, sboxKey string, jinfo driverapi.JoinInfo, options map[string]interface{}) error { + defer osl.InitOSContext()() + n, err := d.getNetwork(nid) + if err != nil { + return err + } + endpoint := n.endpoint(eid) + if endpoint == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + // generate a name for the iface that will be renamed to eth0 in the sbox + containerIfName, err := netutils.GenerateIfaceName(vethPrefix, vethLen) + if err != nil { + return fmt.Errorf("error generating an interface name: %s", err) + } + // create the netlink macvlan interface + vethName, err := createMacVlan(containerIfName, n.config.Parent, n.config.MacvlanMode) + if err != nil { + return err + } + // bind the generated iface name to the endpoint + endpoint.srcName = vethName + ep := n.endpoint(eid) + if ep == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + // parse and match the endpoint address with the available v4 subnets + if len(n.config.Ipv4Subnets) > 0 { + s := n.getSubnetforIPv4(ep.addr) + if s == nil { + return fmt.Errorf("could not find a valid ipv4 subnet for endpoint %s", eid) + } + v4gw, _, err := net.ParseCIDR(s.GwIP) + if err != nil { + return fmt.Errorf("gatway %s is not a valid ipv4 address: %v", s.GwIP, err) + } + err = jinfo.SetGateway(v4gw) + if err != nil { + return err + } + logrus.Debugf("Macvlan Endpoint Joined with IPv4_Addr: %s, Gateway: %s, MacVlan_Mode: %s, Parent: %s", + ep.addr.IP.String(), v4gw.String(), n.config.MacvlanMode, n.config.Parent) + } + // parse and match the endpoint address with the available v6 subnets + if len(n.config.Ipv6Subnets) > 0 { + s := n.getSubnetforIPv6(ep.addrv6) + if s == nil { + return fmt.Errorf("could not find a valid ipv6 subnet for endpoint %s", eid) + } + v6gw, _, err := net.ParseCIDR(s.GwIP) + if err != nil { + return fmt.Errorf("gatway %s is not a valid ipv6 address: %v", s.GwIP, err) + } + err = jinfo.SetGatewayIPv6(v6gw) + if err != nil { + return err + } + logrus.Debugf("Macvlan Endpoint Joined with IPv6_Addr: %s Gateway: %s MacVlan_Mode: %s, Parent: %s", + ep.addrv6.IP.String(), v6gw.String(), n.config.MacvlanMode, n.config.Parent) + } + iNames := jinfo.InterfaceName() + err = iNames.SetNames(vethName, containerVethPrefix) + if err != nil { + return err + } + + return nil +} + +// Leave method is invoked when a Sandbox detaches from an endpoint. +func (d *driver) Leave(nid, eid string) error { + network, err := d.getNetwork(nid) + if err != nil { + return err + } + endpoint, err := network.getEndpoint(eid) + if err != nil { + return err + } + if endpoint == nil { + return fmt.Errorf("could not find endpoint with id %s", eid) + } + + return nil +} + +// getSubnetforIP returns the ipv4 subnet to which the given IP belongs +func (n *network) getSubnetforIPv4(ip *net.IPNet) *ipv4Subnet { + for _, s := range n.config.Ipv4Subnets { + _, snet, err := net.ParseCIDR(s.SubnetIP) + if err != nil { + return nil + } + // first check if the mask lengths are the same + i, _ := snet.Mask.Size() + j, _ := ip.Mask.Size() + if i != j { + continue + } + if snet.Contains(ip.IP) { + return s + } + } + + return nil +} + +// getSubnetforIPv6 returns the ipv6 subnet to which the given IP belongs +func (n *network) getSubnetforIPv6(ip *net.IPNet) *ipv6Subnet { + for _, s := range n.config.Ipv6Subnets { + _, snet, err := net.ParseCIDR(s.SubnetIP) + if err != nil { + return nil + } + // first check if the mask lengths are the same + i, _ := snet.Mask.Size() + j, _ := ip.Mask.Size() + if i != j { + continue + } + if snet.Contains(ip.IP) { + return s + } + } + + return nil +} diff --git a/drivers/macvlan/macvlan_network.go b/drivers/macvlan/macvlan_network.go new file mode 100644 index 0000000000..3b46a335f8 --- /dev/null +++ b/drivers/macvlan/macvlan_network.go @@ -0,0 +1,235 @@ +package macvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/types" +) + +// CreateNetwork the network for the specified driver type +func (d *driver) CreateNetwork(nid string, option map[string]interface{}, ipV4Data, ipV6Data []driverapi.IPAMData) error { + // parse and validate the config and bind to networkConfiguration + config, err := parseNetworkOptions(nid, option) + if err != nil { + return err + } + config.ID = nid + err = config.processIPAM(nid, ipV4Data, ipV6Data) + if err != nil { + return err + } + // verify the macvlan mode from -o macvlan_mode option + switch config.MacvlanMode { + case "", modeBridge: + // default to macvlan bridge mode if -o macvlan_mode is empty + config.MacvlanMode = modeBridge + case modeOpt: + config.MacvlanMode = modeOpt + case modePassthru: + config.MacvlanMode = modePassthru + case modeVepa: + config.MacvlanMode = modeVepa + default: + return fmt.Errorf("requested macvlan mode '%s' is not valid, 'bridge' mode is the macvlan driver default", config.MacvlanMode) + } + // loopback is not a valid parent link + if config.Parent == "lo" { + return fmt.Errorf("loopback interface is not a valid %s parent link", macvlanType) + } + // if parent interface not specified, create a dummy type link to use named dummy+net_id + if config.Parent == "" { + config.Parent = getDummyName(stringid.TruncateID(config.ID)) + // empty parent and --internal are handled the same. Set here to update k/v + config.Internal = true + } + err = d.createNetwork(config) + if err != nil { + return err + } + // update persistent db, rollback on fail + err = d.storeUpdate(config) + if err != nil { + d.deleteNetwork(config.ID) + logrus.Debugf("encoutered an error rolling back a network create for %s : %v", config.ID, err) + return err + } + + return nil +} + +// createNetwork is used by new network callbacks and persistent network cache +func (d *driver) createNetwork(config *configuration) error { + // fail the network create if the macvlan kernel module is unavailable + if err := kernelSupport(macvlanType); err != nil { + return err + } + networkList := d.getNetworks() + for _, nw := range networkList { + if config.Parent == nw.config.Parent { + return fmt.Errorf("network %s is already using parent interface %s", + getDummyName(stringid.TruncateID(nw.config.ID)), config.Parent) + } + } + if !parentExists(config.Parent) { + // if the --internal flag is set, create a dummy link + if config.Internal { + err := createDummyLink(config.Parent, getDummyName(stringid.TruncateID(config.ID))) + if err != nil { + return err + } + config.CreatedSlaveLink = true + // notify the user in logs they have limited comunicatins + if config.Parent == getDummyName(stringid.TruncateID(config.ID)) { + logrus.Debugf("Empty -o parent= and --internal flags limit communications to other containers inside of network: %s", + config.Parent) + } + } else { + // if the subinterface parent_iface.vlan_id checks do not pass, return err. + // a valid example is 'eth0.10' for a parent iface 'eth0' with a vlan id '10' + err := createVlanLink(config.Parent) + if err != nil { + return err + } + // if driver created the networks slave link, record it for future deletion + config.CreatedSlaveLink = true + } + } + n := &network{ + id: config.ID, + driver: d, + endpoints: endpointTable{}, + config: config, + } + // add the *network + d.addNetwork(n) + + return nil +} + +// DeleteNetwork the network for the specified driver type +func (d *driver) DeleteNetwork(nid string) error { + n := d.network(nid) + if n == nil { + return fmt.Errorf("network id %s not found", nid) + } + // if the driver created the slave interface, delete it, otherwise leave it + if ok := n.config.CreatedSlaveLink; ok { + // if the interface exists, only delete if it matches iface.vlan or dummy.net_id naming + if ok := parentExists(n.config.Parent); ok { + // only delete the link if it is named the net_id + if n.config.Parent == getDummyName(stringid.TruncateID(nid)) { + err := delDummyLink(n.config.Parent) + if err != nil { + logrus.Debugf("link %s was not deleted, continuing the delete network operation: %v", + n.config.Parent, err) + } + } else { + // only delete the link if it matches iface.vlan naming + err := delVlanLink(n.config.Parent) + if err != nil { + logrus.Debugf("link %s was not deleted, continuing the delete network operation: %v", + n.config.Parent, err) + } + } + } + } + // delete the *network + d.deleteNetwork(nid) + // delete the network record from persistent cache + err := d.storeDelete(n.config) + if err != nil { + return fmt.Errorf("error deleting deleting id %s from datastore: %v", nid, err) + } + return nil +} + +// parseNetworkOptions parse docker network options +func parseNetworkOptions(id string, option options.Generic) (*configuration, error) { + var ( + err error + config = &configuration{} + ) + // parse generic labels first + if genData, ok := option[netlabel.GenericData]; ok && genData != nil { + if config, err = parseNetworkGenericOptions(genData); err != nil { + return nil, err + } + } + // setting the parent to "" will trigger an isolated network dummy parent link + if _, ok := option[netlabel.Internal]; ok { + config.Internal = true + // empty --parent= and --internal are handled the same. + config.Parent = "" + } + + return config, nil +} + +// parseNetworkGenericOptions parse generic driver docker network options +func parseNetworkGenericOptions(data interface{}) (*configuration, error) { + var ( + err error + config *configuration + ) + switch opt := data.(type) { + case *configuration: + config = opt + case map[string]string: + config = &configuration{} + err = config.fromOptions(opt) + case options.Generic: + var opaqueConfig interface{} + if opaqueConfig, err = options.GenerateFromModel(opt, config); err == nil { + config = opaqueConfig.(*configuration) + } + default: + err = types.BadRequestErrorf("unrecognized network configuration format: %v", opt) + } + + return config, err +} + +// fromOptions binds the generic options to networkConfiguration to cache +func (config *configuration) fromOptions(labels map[string]string) error { + for label, value := range labels { + switch label { + case parentOpt: + // parse driver option '-o parent' + config.Parent = value + case driverModeOpt: + // parse driver option '-o macvlan_mode' + config.MacvlanMode = value + } + } + + return nil +} + +// processIPAM parses v4 and v6 IP information and binds it to the network configuration +func (config *configuration) processIPAM(id string, ipamV4Data, ipamV6Data []driverapi.IPAMData) error { + if len(ipamV4Data) > 0 { + for _, ipd := range ipamV4Data { + s := &ipv4Subnet{ + SubnetIP: ipd.Pool.String(), + GwIP: ipd.Gateway.String(), + } + config.Ipv4Subnets = append(config.Ipv4Subnets, s) + } + } + if len(ipamV6Data) > 0 { + for _, ipd := range ipamV6Data { + s := &ipv6Subnet{ + SubnetIP: ipd.Pool.String(), + GwIP: ipd.Gateway.String(), + } + config.Ipv6Subnets = append(config.Ipv6Subnets, s) + } + } + + return nil +} diff --git a/drivers/macvlan/macvlan_setup.go b/drivers/macvlan/macvlan_setup.go new file mode 100644 index 0000000000..d6d97a1b3a --- /dev/null +++ b/drivers/macvlan/macvlan_setup.go @@ -0,0 +1,232 @@ +package macvlan + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/osl" + "github.com/vishvananda/netlink" +) + +const ( + dummyPrefix = "dm-" // macvlan prefix for dummy parent interface + macvlanKernelVer = "3.9" // minimum ipvlan kernel version support +) + +// Create the macvlan slave specifying the source name +func createMacVlan(containerIfName, parent, macvlanMode string) (string, error) { + defer osl.InitOSContext()() + + // Set the macvlan mode. Default is bridge mode + mode, err := setMacVlanMode(macvlanMode) + if err != nil { + return "", fmt.Errorf("Unsupported %s macvlan mode: %v", macvlanMode, err) + } + // verify the Docker host interface acting as the macvlan parent iface exists + if !parentExists(parent) { + return "", fmt.Errorf("the requested parent interface %s was not found on the Docker host", parent) + } + // Get the link for the master index (Example: the docker host eth iface) + parentLink, err := netlink.LinkByName(parent) + if err != nil { + return "", fmt.Errorf("error occoured looking up the %s parent iface %s error: %s", macvlanType, parent, err) + } + // Create a macvlan link + macvlan := &netlink.Macvlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: containerIfName, + ParentIndex: parentLink.Attrs().Index, + }, + Mode: mode, + } + if err := netlink.LinkAdd(macvlan); err != nil { + // If a user creates a macvlan and ipvlan on same parent, only one slave iface can be active at a time. + return "", fmt.Errorf("failed to create the %s port: %v", macvlanType, err) + } + + return macvlan.Attrs().Name, nil +} + +// setMacVlanMode setter for one of the four macvlan port types +func setMacVlanMode(mode string) (netlink.MacvlanMode, error) { + switch mode { + case modePrivate: + return netlink.MACVLAN_MODE_PRIVATE, nil + case modeVepa: + return netlink.MACVLAN_MODE_VEPA, nil + case modeBridge: + return netlink.MACVLAN_MODE_BRIDGE, nil + case modePassthru: + return netlink.MACVLAN_MODE_PASSTHRU, nil + default: + return 0, fmt.Errorf("unknown macvlan mode: %s", mode) + } +} + +// parentExists check if the specified interface exists in the default namespace +func parentExists(ifaceStr string) bool { + _, err := netlink.LinkByName(ifaceStr) + if err != nil { + return false + } + + return true +} + +// kernelSupport for the necessary kernel module for the driver type +func kernelSupport(networkTpe string) error { + // attempt to load the module, silent if successful or already loaded + exec.Command("modprobe", macvlanType).Run() + f, err := os.Open("/proc/modules") + if err != nil { + return err + } + defer f.Close() + s := bufio.NewScanner(f) + for s.Scan() { + if strings.Contains(s.Text(), macvlanType) { + return nil + } + } + + return fmt.Errorf("unable to load the Linux kernel module %s", macvlanType) +} + +// createVlanLink parses sub-interfaces and vlan id for creation +func createVlanLink(parentName string) error { + if strings.Contains(parentName, ".") { + parent, vidInt, err := parseVlan(parentName) + if err != nil { + return err + } + // VLAN identifier or VID is a 12-bit field specifying the VLAN to which the frame belongs + if vidInt > 4094 || vidInt < 1 { + return fmt.Errorf("vlan id must be between 1-4094, received: %d", vidInt) + } + // get the parent link to attach a vlan subinterface + parentLink, err := netlink.LinkByName(parent) + if err != nil { + return fmt.Errorf("failed to find master interface %s on the Docker host: %v", parent, err) + } + vlanLink := &netlink.Vlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: parentName, + ParentIndex: parentLink.Attrs().Index, + }, + VlanId: vidInt, + } + // create the subinterface + if err := netlink.LinkAdd(vlanLink); err != nil { + return fmt.Errorf("failed to create %s vlan link: %v", vlanLink.Name, err) + } + // Bring the new netlink iface up + if err := netlink.LinkSetUp(vlanLink); err != nil { + return fmt.Errorf("failed to enable %s the macvlan parent link %v", vlanLink.Name, err) + } + logrus.Debugf("Added a vlan tagged netlink subinterface: %s with a vlan id: %d", parentName, vidInt) + return nil + } + + return fmt.Errorf("invalid subinterface vlan name %s, example formatting is eth0.10", parentName) +} + +// delVlanLink verifies only sub-interfaces with a vlan id get deleted +func delVlanLink(linkName string) error { + if strings.Contains(linkName, ".") { + _, _, err := parseVlan(linkName) + if err != nil { + return err + } + // delete the vlan subinterface + vlanLink, err := netlink.LinkByName(linkName) + if err != nil { + return fmt.Errorf("failed to find interface %s on the Docker host : %v", linkName, err) + } + // verify a parent interface isn't being deleted + if vlanLink.Attrs().ParentIndex == 0 { + return fmt.Errorf("interface %s does not appear to be a slave device: %v", linkName, err) + } + // delete the macvlan slave device + if err := netlink.LinkDel(vlanLink); err != nil { + return fmt.Errorf("failed to delete %s link: %v", linkName, err) + } + logrus.Debugf("Deleted a vlan tagged netlink subinterface: %s", linkName) + } + // if the subinterface doesn't parse to iface.vlan_id leave the interface in + // place since it could be a user specified name not created by the driver. + return nil +} + +// parseVlan parses and verifies a slave interface name: -o parent=eth0.10 +func parseVlan(linkName string) (string, int, error) { + // parse -o parent=eth0.10 + splitName := strings.Split(linkName, ".") + if len(splitName) != 2 { + return "", 0, fmt.Errorf("required interface name format is: name.vlan_id, ex. eth0.10 for vlan 10, instead received %s", linkName) + } + parent, vidStr := splitName[0], splitName[1] + // validate type and convert vlan id to int + vidInt, err := strconv.Atoi(vidStr) + if err != nil { + return "", 0, fmt.Errorf("unable to parse a valid vlan id from: %s (ex. eth0.10 for vlan 10)", vidStr) + } + // Check if the interface exists + if !parentExists(parent) { + return "", 0, fmt.Errorf("-o parent interface does was not found on the host: %s", parent) + } + + return parent, vidInt, nil +} + +// createDummyLink creates a dummy0 parent link +func createDummyLink(dummyName, truncNetID string) error { + // create a parent interface since one was not specified + parent := &netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: dummyName, + }, + } + if err := netlink.LinkAdd(parent); err != nil { + return err + } + parentDummyLink, err := netlink.LinkByName(dummyName) + if err != nil { + return fmt.Errorf("error occoured looking up the %s parent iface %s error: %s", macvlanType, dummyName, err) + } + // bring the new netlink iface up + if err := netlink.LinkSetUp(parentDummyLink); err != nil { + return fmt.Errorf("failed to enable %s the macvlan parent link: %v", dummyName, err) + } + + return nil +} + +// delDummyLink deletes the link type dummy used when -o parent is not passed +func delDummyLink(linkName string) error { + // delete the vlan subinterface + dummyLink, err := netlink.LinkByName(linkName) + if err != nil { + return fmt.Errorf("failed to find link %s on the Docker host : %v", linkName, err) + } + // verify a parent interface is being deleted + if dummyLink.Attrs().ParentIndex != 0 { + return fmt.Errorf("link %s is not a parent dummy interface", linkName) + } + // delete the macvlan dummy device + if err := netlink.LinkDel(dummyLink); err != nil { + return fmt.Errorf("failed to delete the dummy %s link: %v", linkName, err) + } + logrus.Debugf("Deleted a dummy parent link: %s", linkName) + + return nil +} + +// getDummyName returns the name of a dummy parent with truncated net ID and driver prefix +func getDummyName(netID string) string { + return fmt.Sprintf("%s%s", dummyPrefix, netID) +} diff --git a/drivers/macvlan/macvlan_setup_test.go b/drivers/macvlan/macvlan_setup_test.go new file mode 100644 index 0000000000..49a8780c88 --- /dev/null +++ b/drivers/macvlan/macvlan_setup_test.go @@ -0,0 +1,103 @@ +package macvlan + +import ( + "testing" + + "github.com/vishvananda/netlink" +) + +// TestValidateLink tests the parentExists function +func TestValidateLink(t *testing.T) { + validIface := "lo" + invalidIface := "foo12345" + + // test a valid parent interface validation + if ok := parentExists(validIface); !ok { + t.Fatalf("failed validating loopback %s", validIface) + } + // test a invalid parent interface validation + if ok := parentExists(invalidIface); ok { + t.Fatalf("failed to invalidate interface %s", invalidIface) + } +} + +// TestValidateSubLink tests valid 802.1q naming convention +func TestValidateSubLink(t *testing.T) { + validSubIface := "lo.10" + invalidSubIface1 := "lo" + invalidSubIface2 := "lo:10" + invalidSubIface3 := "foo123.456" + + // test a valid parent_iface.vlan_id + _, _, err := parseVlan(validSubIface) + if err != nil { + t.Fatalf("failed subinterface validation: %v", err) + } + // test a invalid vid with a valid parent link + _, _, err = parseVlan(invalidSubIface1) + if err == nil { + t.Fatalf("failed subinterface validation test: %s", invalidSubIface1) + } + // test a valid vid with a valid parent link with a invalid delimiter + _, _, err = parseVlan(invalidSubIface2) + if err == nil { + t.Fatalf("failed subinterface validation test: %v", invalidSubIface2) + } + // test a invalid parent link with a valid vid + _, _, err = parseVlan(invalidSubIface3) + if err == nil { + t.Fatalf("failed subinterface validation test: %v", invalidSubIface3) + } +} + +// TestSetMacVlanMode tests the macvlan mode setter +func TestSetMacVlanMode(t *testing.T) { + // test macvlan bridge mode + mode, err := setMacVlanMode(modeBridge) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.MACVLAN_MODE_BRIDGE { + t.Fatalf("expected %d got %d", netlink.MACVLAN_MODE_BRIDGE, mode) + } + // test macvlan passthrough mode + mode, err = setMacVlanMode(modePassthru) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.MACVLAN_MODE_PASSTHRU { + t.Fatalf("expected %d got %d", netlink.MACVLAN_MODE_PASSTHRU, mode) + } + // test macvlan private mode + mode, err = setMacVlanMode(modePrivate) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.MACVLAN_MODE_PRIVATE { + t.Fatalf("expected %d got %d", netlink.MACVLAN_MODE_PRIVATE, mode) + } + // test macvlan vepa mode + mode, err = setMacVlanMode(modeVepa) + if err != nil { + t.Fatalf("error parsing %v vlan mode: %v", mode, err) + } + if mode != netlink.MACVLAN_MODE_VEPA { + t.Fatalf("expected %d got %d", netlink.MACVLAN_MODE_VEPA, mode) + } + // test invalid mode + mode, err = setMacVlanMode("foo") + if err == nil { + t.Fatal("invalid macvlan mode should have returned an error") + } + if mode != 0 { + t.Fatalf("expected 0 got %d", mode) + } + // test null mode + mode, err = setMacVlanMode("") + if err == nil { + t.Fatal("invalid macvlan mode should have returned an error") + } + if mode != 0 { + t.Fatalf("expected 0 got %d", mode) + } +} diff --git a/drivers/macvlan/macvlan_state.go b/drivers/macvlan/macvlan_state.go new file mode 100644 index 0000000000..dd2a60ce4b --- /dev/null +++ b/drivers/macvlan/macvlan_state.go @@ -0,0 +1,113 @@ +package macvlan + +import ( + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/osl" + "github.com/docker/libnetwork/types" +) + +func (d *driver) network(nid string) *network { + d.Lock() + n, ok := d.networks[nid] + d.Unlock() + if !ok { + logrus.Errorf("network id %s not found", nid) + } + + return n +} + +func (d *driver) addNetwork(n *network) { + d.Lock() + d.networks[n.id] = n + d.Unlock() +} + +func (d *driver) deleteNetwork(nid string) { + d.Lock() + delete(d.networks, nid) + d.Unlock() +} + +// getNetworks Safely returns a slice of existng networks +func (d *driver) getNetworks() []*network { + d.Lock() + defer d.Unlock() + + ls := make([]*network, 0, len(d.networks)) + for _, nw := range d.networks { + ls = append(ls, nw) + } + + return ls +} + +func (n *network) endpoint(eid string) *endpoint { + n.Lock() + defer n.Unlock() + + return n.endpoints[eid] +} + +func (n *network) addEndpoint(ep *endpoint) { + n.Lock() + n.endpoints[ep.id] = ep + n.Unlock() +} + +func (n *network) deleteEndpoint(eid string) { + n.Lock() + delete(n.endpoints, eid) + n.Unlock() +} + +func (n *network) getEndpoint(eid string) (*endpoint, error) { + n.Lock() + defer n.Unlock() + if eid == "" { + return nil, fmt.Errorf("endpoint id %s not found", eid) + } + if ep, ok := n.endpoints[eid]; ok { + return ep, nil + } + + return nil, nil +} + +func validateID(nid, eid string) error { + if nid == "" { + return fmt.Errorf("invalid network id") + } + if eid == "" { + return fmt.Errorf("invalid endpoint id") + } + return nil +} + +func (n *network) sandbox() osl.Sandbox { + n.Lock() + defer n.Unlock() + + return n.sbox +} + +func (n *network) setSandbox(sbox osl.Sandbox) { + n.Lock() + n.sbox = sbox + n.Unlock() +} + +func (d *driver) getNetwork(id string) (*network, error) { + d.Lock() + defer d.Unlock() + if id == "" { + return nil, types.BadRequestErrorf("invalid network id: %s", id) + } + if nw, ok := d.networks[id]; ok { + return nw, nil + } + + return nil, types.NotFoundErrorf("network not found: %s", id) +} diff --git a/drivers/macvlan/macvlan_store.go b/drivers/macvlan/macvlan_store.go new file mode 100644 index 0000000000..492ea93f71 --- /dev/null +++ b/drivers/macvlan/macvlan_store.go @@ -0,0 +1,219 @@ +package macvlan + +import ( + "encoding/json" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/libkv/store/boltdb" + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/discoverapi" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/types" +) + +const macvlanPrefix = "macvlan" // prefix used for persistent driver storage + +// networkConfiguration for this driver's network specific configuration +type configuration struct { + ID string + Mtu int + dbIndex uint64 + dbExists bool + Internal bool + Parent string + MacvlanMode string + CreatedSlaveLink bool + Ipv4Subnets []*ipv4Subnet + Ipv6Subnets []*ipv6Subnet +} + +type ipv4Subnet struct { + SubnetIP string + GwIP string +} + +type ipv6Subnet struct { + SubnetIP string + GwIP string +} + +// initStore drivers are responsible for caching their own persistent state +func (d *driver) initStore(option map[string]interface{}) error { + if data, ok := option[netlabel.LocalKVClient]; ok { + var err error + dsc, ok := data.(discoverapi.DatastoreConfigData) + if !ok { + return types.InternalErrorf("incorrect data in datastore configuration: %v", data) + } + d.store, err = datastore.NewDataStoreFromConfig(dsc) + if err != nil { + return types.InternalErrorf("macvlan driver failed to initialize data store: %v", err) + } + + return d.populateNetworks() + } + + return nil +} + +// populateNetworks is invoked at driver init to recreate persistently stored networks +func (d *driver) populateNetworks() error { + kvol, err := d.store.List(datastore.Key(macvlanPrefix), &configuration{}) + if err != nil && err != datastore.ErrKeyNotFound && err != boltdb.ErrBoltBucketNotFound { + return fmt.Errorf("failed to get macvlan network configurations from store: %v", err) + } + // If empty it simply means no macvlan networks have been created yet + if err == datastore.ErrKeyNotFound { + return nil + } + for _, kvo := range kvol { + config := kvo.(*configuration) + if err = d.createNetwork(config); err != nil { + logrus.Warnf("Could not create macvlan network for id %s from persistent state", config.ID) + } + } + + return nil +} + +// storeUpdate used to update persistent macvlan network records as they are created +func (d *driver) storeUpdate(kvObject datastore.KVObject) error { + if d.store == nil { + logrus.Warnf("macvlan store not initialized. kv object %s is not added to the store", datastore.Key(kvObject.Key()...)) + return nil + } + if err := d.store.PutObjectAtomic(kvObject); err != nil { + return fmt.Errorf("failed to update macvlan store for object type %T: %v", kvObject, err) + } + + return nil +} + +// storeDelete used to delete macvlan records from persistent cache as they are deleted +func (d *driver) storeDelete(kvObject datastore.KVObject) error { + if d.store == nil { + logrus.Debugf("macvlan store not initialized. kv object %s is not deleted from store", datastore.Key(kvObject.Key()...)) + return nil + } +retry: + if err := d.store.DeleteObjectAtomic(kvObject); err != nil { + if err == datastore.ErrKeyModified { + if err := d.store.GetObject(datastore.Key(kvObject.Key()...), kvObject); err != nil { + return fmt.Errorf("could not update the kvobject to latest when trying to delete: %v", err) + } + goto retry + } + return err + } + + return nil +} + +func (config *configuration) MarshalJSON() ([]byte, error) { + nMap := make(map[string]interface{}) + nMap["ID"] = config.ID + nMap["Mtu"] = config.Mtu + nMap["Parent"] = config.Parent + nMap["MacvlanMode"] = config.MacvlanMode + nMap["Internal"] = config.Internal + nMap["CreatedSubIface"] = config.CreatedSlaveLink + if len(config.Ipv4Subnets) > 0 { + iis, err := json.Marshal(config.Ipv4Subnets) + if err != nil { + return nil, err + } + nMap["Ipv4Subnets"] = string(iis) + } + if len(config.Ipv6Subnets) > 0 { + iis, err := json.Marshal(config.Ipv6Subnets) + if err != nil { + return nil, err + } + nMap["Ipv6Subnets"] = string(iis) + } + + return json.Marshal(nMap) +} + +func (config *configuration) UnmarshalJSON(b []byte) error { + var ( + err error + nMap map[string]interface{} + ) + + if err = json.Unmarshal(b, &nMap); err != nil { + return err + } + config.ID = nMap["ID"].(string) + config.Mtu = int(nMap["Mtu"].(float64)) + config.Parent = nMap["Parent"].(string) + config.MacvlanMode = nMap["MacvlanMode"].(string) + config.Internal = nMap["Internal"].(bool) + config.CreatedSlaveLink = nMap["CreatedSubIface"].(bool) + if v, ok := nMap["Ipv4Subnets"]; ok { + if err := json.Unmarshal([]byte(v.(string)), &config.Ipv4Subnets); err != nil { + return err + } + } + if v, ok := nMap["Ipv6Subnets"]; ok { + if err := json.Unmarshal([]byte(v.(string)), &config.Ipv6Subnets); err != nil { + return err + } + } + + return nil +} + +func (config *configuration) Key() []string { + return []string{macvlanPrefix, config.ID} +} + +func (config *configuration) KeyPrefix() []string { + return []string{macvlanPrefix} +} + +func (config *configuration) Value() []byte { + b, err := json.Marshal(config) + if err != nil { + return nil + } + + return b +} + +func (config *configuration) SetValue(value []byte) error { + return json.Unmarshal(value, config) +} + +func (config *configuration) Index() uint64 { + return config.dbIndex +} + +func (config *configuration) SetIndex(index uint64) { + config.dbIndex = index + config.dbExists = true +} + +func (config *configuration) Exists() bool { + return config.dbExists +} + +func (config *configuration) Skip() bool { + return false +} + +func (config *configuration) New() datastore.KVObject { + return &configuration{} +} + +func (config *configuration) CopyTo(o datastore.KVObject) error { + dstNcfg := o.(*configuration) + *dstNcfg = *config + + return nil +} + +func (config *configuration) DataScope() string { + return datastore.LocalScope +} diff --git a/drivers/macvlan/macvlan_test.go b/drivers/macvlan/macvlan_test.go new file mode 100644 index 0000000000..243df5fc0b --- /dev/null +++ b/drivers/macvlan/macvlan_test.go @@ -0,0 +1,60 @@ +package macvlan + +import ( + "testing" + + "github.com/docker/libnetwork/driverapi" + _ "github.com/docker/libnetwork/testutils" +) + +const testNetworkType = "macvlan" + +type driverTester struct { + t *testing.T + d *driver +} + +func (dt *driverTester) RegisterDriver(name string, drv driverapi.Driver, + cap driverapi.Capability) error { + if name != testNetworkType { + dt.t.Fatalf("Expected driver register name to be %q. Instead got %q", + testNetworkType, name) + } + + if _, ok := drv.(*driver); !ok { + dt.t.Fatalf("Expected driver type to be %T. Instead got %T", + &driver{}, drv) + } + + dt.d = drv.(*driver) + return nil +} + +func TestMacvlanInit(t *testing.T) { + if err := Init(&driverTester{t: t}, nil); err != nil { + t.Fatal(err) + } +} + +func TestMacvlanNilConfig(t *testing.T) { + dt := &driverTester{t: t} + if err := Init(dt, nil); err != nil { + t.Fatal(err) + } + + if err := dt.d.initStore(nil); err != nil { + t.Fatal(err) + } +} + +func TestMacvlanType(t *testing.T) { + dt := &driverTester{t: t} + if err := Init(dt, nil); err != nil { + t.Fatal(err) + } + + if dt.d.Type() != testNetworkType { + t.Fatalf("Expected Type() to return %q. Instead got %q", testNetworkType, + dt.d.Type()) + } +} diff --git a/drivers_experimental_linux.go b/drivers_experimental_linux.go new file mode 100644 index 0000000000..49f7b9bb36 --- /dev/null +++ b/drivers_experimental_linux.go @@ -0,0 +1,15 @@ +// +build experimental + +package libnetwork + +import ( + "github.com/docker/libnetwork/drivers/ipvlan" + "github.com/docker/libnetwork/drivers/macvlan" +) + +func additionalDrivers() []initializer { + return []initializer{ + {macvlan.Init, "macvlan"}, + {ipvlan.Init, "ipvlan"}, + } +} diff --git a/drivers_linux.go b/drivers_linux.go index 4d31b986c2..df8b4d734b 100644 --- a/drivers_linux.go +++ b/drivers_linux.go @@ -9,11 +9,14 @@ import ( ) func getInitializers() []initializer { - return []initializer{ + in := []initializer{ {bridge.Init, "bridge"}, {host.Init, "host"}, {null.Init, "null"}, {remote.Init, "remote"}, {overlay.Init, "overlay"}, } + + in = append(in, additionalDrivers()...) + return in } diff --git a/drivers_stub_linux.go b/drivers_stub_linux.go new file mode 100644 index 0000000000..e20428c72f --- /dev/null +++ b/drivers_stub_linux.go @@ -0,0 +1,7 @@ +// +build !experimental + +package libnetwork + +func additionalDrivers() []initializer { + return nil +}