From 09a8148e3634e3c267d5ea2f8f2c5beb4eac9dca Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Fri, 7 Aug 2015 16:27:52 +0100 Subject: [PATCH] Factor an API out into a module This takes some of the machinery from CNI and from the rkt networking code, and turns it into a library that can be linked into go apps. Included is an example command-line application that uses the library, called `cnitool`. Other headline changes: * Plugin exec'ing is factored out The motivation here is to factor out the protocol for invoking plugins. To that end, a generalisation of the code from api.go and pkg/plugin/ipam.go goes into pkg/invoke/exec.go. * Move argument-handling and conf-loading into public API The fact that the arguments get turned into an environment for the plugin is incidental to the API; so, provide a way of supplying them as a struct or saying "just use the same arguments as I got" (the latter is for IPAM plugins). --- invoke/args.go | 76 +++++++++++++++++++ invoke/exec.go | 66 ++++++++++++++++ invoke/find.go | 37 +++++++++ ip/cidr.go | 35 --------- ipam/ipam.go | 75 ++++++++++++++++++ plugin/ipam.go | 151 ------------------------------------- skel/skel.go | 8 +- {plugin => types}/args.go | 16 +++- {plugin => types}/types.go | 54 ++++++++++--- 9 files changed, 316 insertions(+), 202 deletions(-) create mode 100644 invoke/args.go create mode 100644 invoke/exec.go create mode 100644 invoke/find.go create mode 100644 ipam/ipam.go delete mode 100644 plugin/ipam.go rename {plugin => types}/args.go (56%) rename {plugin => types}/types.go (73%) diff --git a/invoke/args.go b/invoke/args.go new file mode 100644 index 00000000..6f0a813a --- /dev/null +++ b/invoke/args.go @@ -0,0 +1,76 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 invoke + +import ( + "os" + "strings" +) + +type CNIArgs interface { + // For use with os/exec; i.e., return nil to inherit the + // environment from this process + AsEnv() []string +} + +type inherited struct{} + +var inheritArgsFromEnv inherited + +func (_ *inherited) AsEnv() []string { + return nil +} + +func ArgsFromEnv() CNIArgs { + return &inheritArgsFromEnv +} + +type Args struct { + Command string + ContainerID string + NetNS string + PluginArgs [][2]string + PluginArgsStr string + IfName string + Path string +} + +func (args *Args) AsEnv() []string { + env := os.Environ() + pluginArgsStr := args.PluginArgsStr + if pluginArgsStr == "" { + pluginArgsStr = stringify(args.PluginArgs) + } + + env = append(env, + "CNI_COMMAND="+args.Command, + "CNI_CONTAINERID="+args.ContainerID, + "CNI_NETNS="+args.NetNS, + "CNI_ARGS="+pluginArgsStr, + "CNI_IFNAME="+args.IfName, + "CNI_PATH="+args.Path) + return env +} + +// taken from rkt/networking/net_plugin.go +func stringify(pluginArgs [][2]string) string { + entries := make([]string, len(pluginArgs)) + + for i, kv := range pluginArgs { + entries[i] = strings.Join(kv[:], "=") + } + + return strings.Join(entries, ";") +} diff --git a/invoke/exec.go b/invoke/exec.go new file mode 100644 index 00000000..d7c5b7ab --- /dev/null +++ b/invoke/exec.go @@ -0,0 +1,66 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 invoke + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/appc/cni/pkg/types" +) + +func pluginErr(err error, output []byte) error { + if _, ok := err.(*exec.ExitError); ok { + emsg := types.Error{} + if perr := json.Unmarshal(output, &emsg); perr != nil { + return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) + } + details := "" + if emsg.Details != "" { + details = fmt.Sprintf("; %v", emsg.Details) + } + return fmt.Errorf("%v%v", emsg.Msg, details) + } + + return err +} + +func ExecPlugin(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { + if pluginPath == "" { + return nil, fmt.Errorf("could not find %q plugin", filepath.Base(pluginPath)) + } + + stdout := &bytes.Buffer{} + + c := exec.Cmd{ + Env: args.AsEnv(), + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stdout: stdout, + Stderr: os.Stderr, + } + if err := c.Run(); err != nil { + return nil, pluginErr(err, stdout.Bytes()) + } + + res := &types.Result{} + err := json.Unmarshal(stdout.Bytes(), res) + return res, err +} diff --git a/invoke/find.go b/invoke/find.go new file mode 100644 index 00000000..dfad12bc --- /dev/null +++ b/invoke/find.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 invoke + +import ( + "os" + "path/filepath" + "strings" +) + +func FindInPath(plugin string, path []string) string { + for _, p := range path { + fullname := filepath.Join(p, plugin) + if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { + return fullname + } + } + return "" +} + +// Find returns the full path of the plugin by searching in CNI_PATH +func Find(plugin string) string { + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + return FindInPath(plugin, paths) +} diff --git a/ip/cidr.go b/ip/cidr.go index c9633988..723a1f74 100644 --- a/ip/cidr.go +++ b/ip/cidr.go @@ -15,23 +15,10 @@ package ip import ( - "encoding/json" "math/big" "net" ) -// ParseCIDR takes a string like "10.2.3.1/24" and -// return IPNet with "10.2.3.1" and /24 mask -func ParseCIDR(s string) (*net.IPNet, error) { - ip, ipn, err := net.ParseCIDR(s) - if err != nil { - return nil, err - } - - ipn.IP = ip - return ipn, nil -} - // NextIP returns IP incremented by 1 func NextIP(ip net.IP) net.IP { i := ipToInt(ip) @@ -62,25 +49,3 @@ func Network(ipn *net.IPNet) *net.IPNet { Mask: ipn.Mask, } } - -// like net.IPNet but adds JSON marshalling and unmarshalling -type IPNet net.IPNet - -func (n IPNet) MarshalJSON() ([]byte, error) { - return json.Marshal((*net.IPNet)(&n).String()) -} - -func (n *IPNet) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - tmp, err := ParseCIDR(s) - if err != nil { - return err - } - - *n = IPNet(*tmp) - return nil -} diff --git a/ipam/ipam.go b/ipam/ipam.go new file mode 100644 index 00000000..a76299d7 --- /dev/null +++ b/ipam/ipam.go @@ -0,0 +1,75 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 ipam + +import ( + "fmt" + "os" + + "github.com/appc/cni/pkg/invoke" + "github.com/appc/cni/pkg/ip" + "github.com/appc/cni/pkg/types" + + "github.com/vishvananda/netlink" +) + +func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { + if os.Getenv("CNI_COMMAND") != "ADD" { + return nil, fmt.Errorf("CNI_COMMAND is not ADD") + } + return invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) +} + +func ExecDel(plugin string, netconf []byte) error { + if os.Getenv("CNI_COMMAND") != "DEL" { + return fmt.Errorf("CNI_COMMAND is not DEL") + } + _, err := invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) + return err +} + +// ConfigureIface takes the result of IPAM plugin and +// applies to the ifName interface +func ConfigureIface(ifName string, res *types.Result) error { + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed to set %q UP: %v", ifName, err) + } + + // TODO(eyakubovich): IPv6 + addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) + } + + for _, r := range res.IP4.Routes { + gw := r.GW + if gw == nil { + gw = res.IP4.Gateway + } + if err = ip.AddRoute(&r.Dst, gw, link); err != nil { + // we skip over duplicate routes as we assume the first one wins + if !os.IsExist(err) { + return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) + } + } + } + + return nil +} diff --git a/plugin/ipam.go b/plugin/ipam.go deleted file mode 100644 index f304301f..00000000 --- a/plugin/ipam.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// 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 plugin - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/appc/cni/pkg/ip" - "github.com/vishvananda/netlink" -) - -// Find returns the full path of the plugin by searching in CNI_PATH -func Find(plugin string) string { - paths := strings.Split(os.Getenv("CNI_PATH"), ":") - - for _, p := range paths { - fullname := filepath.Join(p, plugin) - if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { - return fullname - } - } - - return "" -} - -func pluginErr(err error, output []byte) error { - if _, ok := err.(*exec.ExitError); ok { - emsg := Error{} - if perr := json.Unmarshal(output, &emsg); perr != nil { - return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) - } - details := "" - if emsg.Details != "" { - details = fmt.Sprintf("; %v", emsg.Details) - } - return fmt.Errorf("%v%v", emsg.Msg, details) - } - - return err -} - -// ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD. -// Parses and returns resulting IPConfig -func ExecAdd(plugin string, netconf []byte) (*Result, error) { - if os.Getenv("CNI_COMMAND") != "ADD" { - return nil, fmt.Errorf("CNI_COMMAND is not ADD") - } - if plugin == "" { - return nil, fmt.Errorf(`name of IPAM plugin is missing. Please specify a "type" field in the "ipam" section`) - } - - pluginPath := Find(plugin) - if pluginPath == "" { - return nil, fmt.Errorf("could not find %q IPAM plugin", plugin) - } - - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(netconf), - Stdout: stdout, - Stderr: os.Stderr, - } - if err := c.Run(); err != nil { - return nil, pluginErr(err, stdout.Bytes()) - } - - res := &Result{} - err := json.Unmarshal(stdout.Bytes(), res) - return res, err -} - -// ExecDel executes IPAM plugin, assuming CNI_COMMAND == DEL. -func ExecDel(plugin string, netconf []byte) error { - if os.Getenv("CNI_COMMAND") != "DEL" { - return fmt.Errorf("CNI_COMMAND is not DEL") - } - - pluginPath := Find(plugin) - if pluginPath == "" { - return fmt.Errorf("could not find %q plugin", plugin) - } - - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(netconf), - Stdout: stdout, - Stderr: os.Stderr, - } - if err := c.Run(); err != nil { - return pluginErr(err, stdout.Bytes()) - } - return nil -} - -// ConfigureIface takes the result of IPAM plugin and -// applies to the ifName interface -func ConfigureIface(ifName string, res *Result) error { - link, err := netlink.LinkByName(ifName) - if err != nil { - return fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - if err := netlink.LinkSetUp(link); err != nil { - return fmt.Errorf("failed to set %q UP: %v", ifName, err) - } - - // TODO(eyakubovich): IPv6 - addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} - if err = netlink.AddrAdd(link, addr); err != nil { - return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) - } - - for _, r := range res.IP4.Routes { - gw := r.GW - if gw == nil { - gw = res.IP4.Gateway - } - if err = ip.AddRoute(&r.Dst, gw, link); err != nil { - // we skip over duplicate routes as we assume the first one wins - if !os.IsExist(err) { - return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) - } - } - } - - return nil -} diff --git a/skel/skel.go b/skel/skel.go index bf79b91d..d6204dd4 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -22,7 +22,7 @@ import ( "log" "os" - "github.com/appc/cni/pkg/plugin" + "github.com/appc/cni/pkg/types" ) // CmdArgs captures all the arguments passed in to the plugin @@ -93,7 +93,7 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } if err != nil { - if e, ok := err.(*plugin.Error); ok { + if e, ok := err.(*types.Error); ok { // don't wrap Error in Error dieErr(e) } @@ -102,14 +102,14 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } func dieMsg(f string, args ...interface{}) { - e := &plugin.Error{ + e := &types.Error{ Code: 100, Msg: fmt.Sprintf(f, args...), } dieErr(e) } -func dieErr(e *plugin.Error) { +func dieErr(e *types.Error) { if err := e.Print(); err != nil { log.Print("Error writing error JSON to stdout: ", err) } diff --git a/plugin/args.go b/types/args.go similarity index 56% rename from plugin/args.go rename to types/args.go index 274ec66b..68162435 100644 --- a/plugin/args.go +++ b/types/args.go @@ -1,4 +1,18 @@ -package plugin +// Copyright 2015 CoreOS, Inc. +// +// 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 types import ( "encoding" diff --git a/plugin/types.go b/types/types.go similarity index 73% rename from plugin/types.go rename to types/types.go index d5952dde..21ba32d6 100644 --- a/plugin/types.go +++ b/types/types.go @@ -12,16 +12,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package types import ( "encoding/json" "net" "os" - - "github.com/appc/cni/pkg/ip" ) +// like net.IPNet but adds JSON marshalling and unmarshalling +type IPNet net.IPNet + +// ParseCIDR takes a string like "10.2.3.1/24" and +// return IPNet with "10.2.3.1" and /24 mask +func ParseCIDR(s string) (*net.IPNet, error) { + ip, ipn, err := net.ParseCIDR(s) + if err != nil { + return nil, err + } + + ipn.IP = ip + return ipn, nil +} + +func (n IPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(&n).String()) +} + +func (n *IPNet) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + tmp, err := ParseCIDR(s) + if err != nil { + return err + } + + *n = IPNet(*tmp) + return nil +} + // NetConf describes a network. type NetConf struct { Name string `json:"name,omitempty"` @@ -68,23 +100,23 @@ func (e *Error) Print() error { } // net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom ip.IPNet type +// for our custom IPNet type // JSON (un)marshallable types type ipConfig struct { - IP ip.IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []Route `json:"routes,omitempty"` + IP IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []Route `json:"routes,omitempty"` } type route struct { - Dst ip.IPNet `json:"dst"` - GW net.IP `json:"gw,omitempty"` + Dst IPNet `json:"dst"` + GW net.IP `json:"gw,omitempty"` } func (c *IPConfig) MarshalJSON() ([]byte, error) { ipc := ipConfig{ - IP: ip.IPNet(c.IP), + IP: IPNet(c.IP), Gateway: c.Gateway, Routes: c.Routes, } @@ -117,7 +149,7 @@ func (r *Route) UnmarshalJSON(data []byte) error { func (r *Route) MarshalJSON() ([]byte, error) { rt := route{ - Dst: ip.IPNet(r.Dst), + Dst: IPNet(r.Dst), GW: r.GW, }