diff --git a/Documentation/flannel.md b/Documentation/flannel.md new file mode 100644 index 00000000..dcb6a663 --- /dev/null +++ b/Documentation/flannel.md @@ -0,0 +1,86 @@ +# flannel plugin + +## Overview +This plugin is designed to work in conjunction with [flannel](https://github.com/coreos/flannel), a network fabric for containers. +When flannel daemon is started, it outputs a `/run/flannel/subnet.env` file that looks like this: +``` +FLANNEL_SUBNET=10.1.17.0/24 +FLANNEL_MTU=1472 +FLANNEL_IPMASQ=true +``` + +This information reflects the attributes of flannel network on the host. +The flannel CNI plugin uses this information to configure another CNI plugin, such as bridge plugin. + +## Operation +Given the following network configuration file and the contents of `/run/flannel/subnet.env` above, +``` +{ + "name": "mynet", + "type": "flannel" +} +``` +the flannel plugin will generate another network configuration file: +``` +{ + "name": "mynet", + "type": "bridge", + "mtu": 1472, + "ipMasq": false, + "isGateway": true, + "ipam": { + "type": "host-local", + "subnet": "10.1.17.0/24" + } +} +``` + +It will then invoke the bridge plugin, passing it the generated configuration. + +As can be seen from above, the flannel plugin, by default, will delegate to the bridge plugin. +If additional configuration values need to be passed to the bridge plugin, it can be done so via the `delegate` field: +``` +{ + "name": "mynet", + "type": "flannel", + "delegate": { + "bridge": "mynet0", + "mtu": 1400 + } +} +``` + +This supplies a configuration parameter to the bridge plugin -- the created bridge will now be named `mynet0`. +Notice that `mtu` has also been specified and this value will not be overwritten by flannel plugin. + +Additionally, the `delegate` field can be used to select a different kind of plugin altogether. +To use `ipvlan` instead of `bridge`, the following configuratoin can be specified: + +``` +{ + "name": "mynet", + "type": "flannel", + "delegate": { + "type": "ipvlan", + "master": "eth0" + } +} +``` + +## Network configuration reference + +* `name` (string, required): the name of the network +* `type` (string, required): "flannel" +* `subnetFile` (string, optional): full path to the subnet file written out by flanneld. Defaults to /run/flannel/subnet.env +* `delegate` (dictionary, optional): specifies configuration options for the delegated plugin. + +flannel plugin will always set the following fields in the delegated plugin configuration: + +* `name`: value of its "name" field. +* `ipam`: "host-local" type will be used with "subnet" set to `$FLANNEL_SUBNET`. + +flannel plugin will set the following fields in the delegated plugin configuration if they are not present: +* `ipMasq`: the inverse of `$FLANNEL_IPMASQ` +* `mtu`: `$FLANNEL_MTU` + +Additionally, for the bridge plugin, `isGateway` will be set to `true`, if not present. diff --git a/README.md b/README.md index 8d4aba58..b5357920 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Hence we are proposing this specification, along with an initial set of plugins ## Included Plugins This repository includes a number of common plugins that can be found in plugins/ directory. +Please see Documentation/ folder for documentation about particular plugins. ## Running the plugins The scripts/ directory contains two scripts, priv-net-run.sh and docker-run.sh, that can be used to excercise the plugins. diff --git a/build b/build index eebf04da..1821c8c7 100755 --- a/build +++ b/build @@ -13,7 +13,7 @@ export GOPATH=${PWD}/gopath echo "Building plugins" -PLUGINS="plugins/main/* plugins/ipam/*" +PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/*" for d in $PLUGINS; do if [ -d $d ]; then plugin=$(basename $d) diff --git a/plugins/meta/flannel/flannel.go b/plugins/meta/flannel/flannel.go new file mode 100644 index 00000000..458df8f2 --- /dev/null +++ b/plugins/meta/flannel/flannel.go @@ -0,0 +1,216 @@ +// 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. + +// This is a "meta-plugin". It reads in its own netconf, combines it with +// the data from flannel generated subnet file and then invokes a plugin +// like bridge or ipvlan to do the real work. + +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/appc/cni/pkg/plugin" + "github.com/appc/cni/pkg/skel" +) + +const ( + defaultSubnetFile = "/run/flannel/subnet.env" + stateDir = "/var/lib/cni/flannel" +) + +type NetConf struct { + plugin.NetConf + SubnetFile string `json:"subnetFile"` + Delegate map[string]interface{} `json:"delegate"` +} + +type subnetEnv struct { + sn *net.IPNet + mtu uint + ipmasq bool +} + +func loadFlannelNetConf(bytes []byte) (*NetConf, error) { + n := &NetConf{ + SubnetFile: defaultSubnetFile, + } + if err := json.Unmarshal(bytes, n); err != nil { + return nil, fmt.Errorf("failed to load netconf: %v", err) + } + return n, nil +} + +func loadFlannelSubnetEnv(fn string) (*subnetEnv, error) { + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer f.Close() + + se := &subnetEnv{} + + s := bufio.NewScanner(f) + for s.Scan() { + parts := strings.SplitN(s.Text(), "=", 2) + switch parts[0] { + case "FLANNEL_SUBNET": + _, se.sn, err = net.ParseCIDR(parts[1]) + if err != nil { + return nil, err + } + + case "FLANNEL_MTU": + mtu, err := strconv.ParseUint(parts[1], 10, 32) + if err != nil { + return nil, err + } + se.mtu = uint(mtu) + + case "FLANNEL_IPMASQ": + se.ipmasq = parts[1] == "true" + } + } + if err := s.Err(); err != nil { + return nil, err + } + + return se, nil +} + +func saveScratchNetConf(containerID string, netconf []byte) error { + if err := os.MkdirAll(stateDir, 0700); err != nil { + return err + } + path := filepath.Join(stateDir, containerID) + return ioutil.WriteFile(path, netconf, 0600) +} + +func consumeScratchNetConf(containerID string) ([]byte, error) { + path := filepath.Join(stateDir, containerID) + defer os.Remove(path) + + return ioutil.ReadFile(path) +} + +func delegateAdd(cid string, netconf map[string]interface{}) error { + netconfBytes, err := json.Marshal(netconf) + if err != nil { + return fmt.Errorf("error serializing delegate netconf: %v", err) + } + + // save the rendered netconf for cmdDel + if err = saveScratchNetConf(cid, netconfBytes); err != nil { + return err + } + + result, err := plugin.ExecAdd(netconf["type"].(string), netconfBytes) + if err != nil { + return err + } + + return result.Print() +} + +func hasKey(m map[string]interface{}, k string) bool { + _, ok := m[k] + return ok +} + +func isString(i interface{}) bool { + _, ok := i.(string) + return ok +} + +func cmdAdd(args *skel.CmdArgs) error { + n, err := loadFlannelNetConf(args.StdinData) + if err != nil { + return err + } + + fenv, err := loadFlannelSubnetEnv(n.SubnetFile) + if err != nil { + return err + } + + if n.Delegate == nil { + n.Delegate = make(map[string]interface{}) + } else { + if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) { + return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field") + } + if hasKey(n.Delegate, "name") { + return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel") + } + if hasKey(n.Delegate, "ipam") { + return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel") + } + } + + n.Delegate["name"] = n.Name + + if !hasKey(n.Delegate, "type") { + n.Delegate["type"] = "bridge" + } + + if !hasKey(n.Delegate, "ipMasq") { + // if flannel is not doing ipmasq, we should + ipmasq := !fenv.ipmasq + n.Delegate["ipMasq"] = ipmasq + } + + if !hasKey(n.Delegate, "mtu") { + mtu := fenv.mtu + n.Delegate["mtu"] = mtu + } + + if n.Delegate["type"].(string) == "bridge" { + if !hasKey(n.Delegate, "isGateway") { + n.Delegate["isGateway"] = true + } + } + + n.Delegate["ipam"] = map[string]string{ + "type": "host-local", + "subnet": fenv.sn.String(), + } + + return delegateAdd(args.ContainerID, n.Delegate) +} + +func cmdDel(args *skel.CmdArgs) error { + netconfBytes, err := consumeScratchNetConf(args.ContainerID) + if err != nil { + return err + } + + n := &plugin.NetConf{} + if err = json.Unmarshal(netconfBytes, n); err != nil { + return fmt.Errorf("failed to parse netconf: %v", err) + } + + return plugin.ExecDel(n.Type, netconfBytes) +} + +func main() { + skel.PluginMain(cmdAdd, cmdDel) +}