vendor: add CNI invoke, ip, ipam, and hwaddr packages

This commit is contained in:
Dan Williams
2017-04-18 22:04:20 -05:00
parent 7309649a66
commit 6b3bf7ea7b
89 changed files with 14428 additions and 0 deletions

View File

@ -0,0 +1,79 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
}
// Args implements the CNIArgs interface
var _ CNIArgs = &Args{}
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, ";")
}

View File

@ -0,0 +1,53 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
import (
"fmt"
"os"
"path/filepath"
"github.com/containernetworking/cni/pkg/types"
)
func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) {
if os.Getenv("CNI_COMMAND") != "ADD" {
return nil, fmt.Errorf("CNI_COMMAND is not ADD")
}
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
pluginPath, err := FindInPath(delegatePlugin, paths)
if err != nil {
return nil, err
}
return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv())
}
func DelegateDel(delegatePlugin string, netconf []byte) error {
if os.Getenv("CNI_COMMAND") != "DEL" {
return fmt.Errorf("CNI_COMMAND is not DEL")
}
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
pluginPath, err := FindInPath(delegatePlugin, paths)
if err != nil {
return err
}
return ExecPluginWithoutResult(pluginPath, netconf, ArgsFromEnv())
}

View File

@ -0,0 +1,95 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
import (
"fmt"
"os"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
return defaultPluginExec.WithResult(pluginPath, netconf, args)
}
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
}
func GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
return defaultPluginExec.GetVersionInfo(pluginPath)
}
var defaultPluginExec = &PluginExec{
RawExec: &RawExec{Stderr: os.Stderr},
VersionDecoder: &version.PluginDecoder{},
}
type PluginExec struct {
RawExec interface {
ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error)
}
VersionDecoder interface {
Decode(jsonBytes []byte) (version.PluginInfo, error)
}
}
func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
if err != nil {
return nil, err
}
// Plugin must return result in same version as specified in netconf
versionDecoder := &version.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(netconf)
if err != nil {
return nil, err
}
return version.NewResult(confVersion, stdoutBytes)
}
func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
_, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
return err
}
// GetVersionInfo returns the version information available about the plugin.
// For recent-enough plugins, it uses the information returned by the VERSION
// command. For older plugins which do not recognize that command, it reports
// version 0.1.0
func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
args := &Args{
Command: "VERSION",
// set fake values required by plugins built against an older version of skel
NetNS: "dummy",
IfName: "dummy",
Path: "dummy",
}
stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current()))
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, stdin, args.AsEnv())
if err != nil {
if err.Error() == "unknown CNI_COMMAND: VERSION" {
return version.PluginSupports("0.1.0"), nil
}
return nil, err
}
return e.VersionDecoder.Decode(stdoutBytes)
}

View File

@ -0,0 +1,157 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke_test
import (
"encoding/json"
"errors"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/invoke/fakes"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Executing a plugin, unit tests", func() {
var (
pluginExec *invoke.PluginExec
rawExec *fakes.RawExec
versionDecoder *fakes.VersionDecoder
pluginPath string
netconf []byte
cniargs *fakes.CNIArgs
)
BeforeEach(func() {
rawExec = &fakes.RawExec{}
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
versionDecoder = &fakes.VersionDecoder{}
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
pluginExec = &invoke.PluginExec{
RawExec: rawExec,
VersionDecoder: versionDecoder,
}
pluginPath = "/some/plugin/path"
netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`)
cniargs = &fakes.CNIArgs{}
cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
})
Describe("returning a result", func() {
It("unmarshals the result bytes into the Result type", func() {
r, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(err).NotTo(HaveOccurred())
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(len(result.IPs)).To(Equal(1))
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
})
It("passes its arguments through to the rawExec", func() {
pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
_, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(err).To(MatchError("banana"))
})
})
})
Describe("without returning a result", func() {
It("passes its arguments through to the rawExec", func() {
pluginExec.WithoutResult(pluginPath, netconf, cniargs)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
err := pluginExec.WithoutResult(pluginPath, netconf, cniargs)
Expect(err).To(MatchError("banana"))
})
})
})
Describe("discovering the plugin version", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`)
})
It("execs the plugin with the command VERSION", func() {
pluginExec.GetVersionInfo(pluginPath)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION"))
expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()})
Expect(rawExec.ExecPluginCall.Received.StdinData).To(MatchJSON(expectedStdin))
})
It("decodes and returns the version info", func() {
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"}))
Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
_, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).To(MatchError("banana"))
})
})
Context("when the plugin is too old to recognize the VERSION command", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("unknown CNI_COMMAND: VERSION")
})
It("interprets the error as a 0.1.0 version", func() {
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0"))
})
It("sets dummy values for env vars required by very old plugins", func() {
pluginExec.GetVersionInfo(pluginPath)
env := rawExec.ExecPluginCall.Received.Environ
Expect(env).To(ContainElement("CNI_NETNS=dummy"))
Expect(env).To(ContainElement("CNI_IFNAME=dummy"))
Expect(env).To(ContainElement("CNI_PATH=dummy"))
})
})
})
})

View File

@ -0,0 +1,27 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fakes
type CNIArgs struct {
AsEnvCall struct {
Returns struct {
Env []string
}
}
}
func (a *CNIArgs) AsEnv() []string {
return a.AsEnvCall.Returns.Env
}

View File

@ -0,0 +1,36 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fakes
type RawExec struct {
ExecPluginCall struct {
Received struct {
PluginPath string
StdinData []byte
Environ []string
}
Returns struct {
ResultBytes []byte
Error error
}
}
}
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
e.ExecPluginCall.Received.PluginPath = pluginPath
e.ExecPluginCall.Received.StdinData = stdinData
e.ExecPluginCall.Received.Environ = environ
return e.ExecPluginCall.Returns.ResultBytes, e.ExecPluginCall.Returns.Error
}

View File

@ -0,0 +1,34 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fakes
import "github.com/containernetworking/cni/pkg/version"
type VersionDecoder struct {
DecodeCall struct {
Received struct {
JSONBytes []byte
}
Returns struct {
PluginInfo version.PluginInfo
Error error
}
}
}
func (e *VersionDecoder) Decode(jsonData []byte) (version.PluginInfo, error) {
e.DecodeCall.Received.JSONBytes = jsonData
return e.DecodeCall.Returns.PluginInfo, e.DecodeCall.Returns.Error
}

View File

@ -0,0 +1,43 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
import (
"fmt"
"os"
"path/filepath"
)
// FindInPath returns the full path of the plugin by searching in the provided path
func FindInPath(plugin string, paths []string) (string, error) {
if plugin == "" {
return "", fmt.Errorf("no plugin name provided")
}
if len(paths) == 0 {
return "", fmt.Errorf("no paths provided")
}
for _, path := range paths {
for _, fe := range ExecutableFileExtensions {
fullpath := filepath.Join(path, plugin) + fe
if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() {
return fullpath, nil
}
}
}
return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths)
}

View File

@ -0,0 +1,103 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke_test
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/containernetworking/cni/pkg/invoke"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("FindInPath", func() {
var (
multiplePaths []string
pluginName string
plugin2NameWithExt string
plugin2NameWithoutExt string
pluginDir string
anotherTempDir string
)
BeforeEach(func() {
tempDir, err := ioutil.TempDir("", "cni-find")
Expect(err).NotTo(HaveOccurred())
plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin")
Expect(err).NotTo(HaveOccurred())
plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0]
plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name))
Expect(err).NotTo(HaveOccurred())
anotherTempDir, err = ioutil.TempDir("", "nothing-here")
Expect(err).NotTo(HaveOccurred())
multiplePaths = []string{anotherTempDir, tempDir}
pluginDir, pluginName = filepath.Split(plugin.Name())
_, plugin2NameWithExt = filepath.Split(plugin2.Name())
plugin2NameWithoutExt = strings.Split(plugin2NameWithExt, ".")[0]
})
AfterEach(func() {
os.RemoveAll(pluginDir)
os.RemoveAll(anotherTempDir)
})
Context("when multiple paths are provided", func() {
It("returns only the path to the plugin", func() {
pluginPath, err := invoke.FindInPath(pluginName, multiplePaths)
Expect(err).NotTo(HaveOccurred())
Expect(pluginPath).To(Equal(filepath.Join(pluginDir, pluginName)))
})
})
Context("when a plugin name without its file name extension is provided", func() {
It("returns the path to the plugin, including its extension", func() {
pluginPath, err := invoke.FindInPath(plugin2NameWithoutExt, multiplePaths)
Expect(err).NotTo(HaveOccurred())
Expect(pluginPath).To(Equal(filepath.Join(pluginDir, plugin2NameWithExt)))
})
})
Context("when an error occurs", func() {
Context("when no paths are provided", func() {
It("returns an error noting no paths were provided", func() {
_, err := invoke.FindInPath(pluginName, []string{})
Expect(err).To(MatchError("no paths provided"))
})
})
Context("when no plugin is provided", func() {
It("returns an error noting the plugin name wasn't found", func() {
_, err := invoke.FindInPath("", multiplePaths)
Expect(err).To(MatchError("no plugin name provided"))
})
})
Context("when the plugin cannot be found", func() {
It("returns an error noting the path", func() {
pathsWithNothing := []string{anotherTempDir}
_, err := invoke.FindInPath(pluginName, pathsWithNothing)
Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing)))
})
})
})
})

View File

@ -0,0 +1,107 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke_test
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/pkg/version/testhelpers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)
var _ = Describe("GetVersion, integration tests", func() {
var (
pluginDir string
pluginPath string
)
BeforeEach(func() {
pluginDir, err := ioutil.TempDir("", "plugins")
Expect(err).NotTo(HaveOccurred())
pluginPath = filepath.Join(pluginDir, "test-plugin")
})
AfterEach(func() {
Expect(os.RemoveAll(pluginDir)).To(Succeed())
})
DescribeTable("correctly reporting plugin versions",
func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) {
Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed())
versionInfo, err := invoke.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions()))
},
Entry("historical: before VERSION was introduced",
git_ref_v010, plugin_source_no_custom_versions,
version.PluginSupports("0.1.0"),
),
Entry("historical: when VERSION was introduced but plugins couldn't customize it",
git_ref_v020_no_custom_versions, plugin_source_no_custom_versions,
version.PluginSupports("0.1.0", "0.2.0"),
),
Entry("historical: when plugins started reporting their own version list",
git_ref_v020_custom_versions, plugin_source_v020_custom_versions,
version.PluginSupports("0.2.0", "0.999.0"),
),
// this entry tracks the current behavior. Before you change it, ensure
// that its previous behavior is captured in the most recent "historical" entry
Entry("current",
"HEAD", plugin_source_v020_custom_versions,
version.PluginSupports("0.2.0", "0.999.0"),
),
)
})
// a 0.2.0 plugin that can report its own versions
const plugin_source_v020_custom_versions = `package main
import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
"fmt"
)
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
func main() { skel.PluginMain(c, c, version.PluginSupports("0.2.0", "0.999.0")) }
`
const git_ref_v020_custom_versions = "bf31ed15"
// a minimal 0.1.0 / 0.2.0 plugin that cannot report it's own version support
const plugin_source_no_custom_versions = `package main
import "github.com/containernetworking/cni/pkg/skel"
import "fmt"
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
func main() { skel.PluginMain(c, c) }
`
const git_ref_v010 = "2c482f4"
const git_ref_v020_no_custom_versions = "349d66d"

View File

@ -0,0 +1,45 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
"testing"
)
func TestInvoke(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Invoke Suite")
}
const packagePath = "github.com/containernetworking/cni/plugins/test/noop"
var pathToPlugin string
var _ = SynchronizedBeforeSuite(func() []byte {
var err error
pathToPlugin, err = gexec.Build(packagePath)
Expect(err).NotTo(HaveOccurred())
return []byte(pathToPlugin)
}, func(crossNodeData []byte) {
pathToPlugin = string(crossNodeData)
})
var _ = SynchronizedAfterSuite(func() {}, func() {
gexec.CleanupBuildArtifacts()
})

View File

@ -0,0 +1,20 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build darwin dragonfly freebsd linux netbsd opensbd solaris
package invoke
// Valid file extensions for plugin executables.
var ExecutableFileExtensions = []string{""}

View File

@ -0,0 +1,18 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
// Valid file extensions for plugin executables.
var ExecutableFileExtensions = []string{".exe", ""}

View File

@ -0,0 +1,63 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os/exec"
"github.com/containernetworking/cni/pkg/types"
)
type RawExec struct {
Stderr io.Writer
}
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{}
c := exec.Cmd{
Env: environ,
Path: pluginPath,
Args: []string{pluginPath},
Stdin: bytes.NewBuffer(stdinData),
Stdout: stdout,
Stderr: e.Stderr,
}
if err := c.Run(); err != nil {
return nil, pluginErr(err, stdout.Bytes())
}
return stdout.Bytes(), nil
}
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
}

View File

@ -0,0 +1,123 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke_test
import (
"bytes"
"io/ioutil"
"os"
"github.com/containernetworking/cni/pkg/invoke"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("RawExec", func() {
var (
debugFileName string
debug *noop_debug.Debug
environ []string
stdin []byte
execer *invoke.RawExec
)
const reportResult = `{ "some": "result" }`
BeforeEach(func() {
debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name()
debug = &noop_debug.Debug{
ReportResult: reportResult,
ReportStderr: "some stderr message",
}
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
environ = []string{
"CNI_COMMAND=ADD",
"CNI_CONTAINERID=some-container-id",
"CNI_ARGS=DEBUG=" + debugFileName,
"CNI_NETNS=/some/netns/path",
"CNI_PATH=/some/bin/path",
"CNI_IFNAME=some-eth0",
}
stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`)
execer = &invoke.RawExec{}
})
AfterEach(func() {
Expect(os.Remove(debugFileName)).To(Succeed())
})
It("runs the plugin with the given stdin and environment", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
debug, err := noop_debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD"))
Expect(debug.CmdArgs.StdinData).To(Equal(stdin))
Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path"))
})
It("returns the resulting stdout as bytes", func() {
resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
Expect(resultBytes).To(BeEquivalentTo(reportResult))
})
Context("when the Stderr writer is set", func() {
var stderrBuffer *bytes.Buffer
BeforeEach(func() {
stderrBuffer = &bytes.Buffer{}
execer.Stderr = stderrBuffer
})
It("forwards any stderr bytes to the Stderr writer", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
Expect(stderrBuffer.String()).To(Equal("some stderr message"))
})
})
Context("when the plugin errors", func() {
BeforeEach(func() {
debug.ReportError = "banana"
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("wraps and returns the error", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("banana"))
})
})
Context("when the system is unable to execute the plugin", func() {
It("returns the error", func() {
_, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path")))
})
})
})

View File

@ -0,0 +1,51 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"math/big"
"net"
)
// NextIP returns IP incremented by 1
func NextIP(ip net.IP) net.IP {
i := ipToInt(ip)
return intToIP(i.Add(i, big.NewInt(1)))
}
// PrevIP returns IP decremented by 1
func PrevIP(ip net.IP) net.IP {
i := ipToInt(ip)
return intToIP(i.Sub(i, big.NewInt(1)))
}
func ipToInt(ip net.IP) *big.Int {
if v := ip.To4(); v != nil {
return big.NewInt(0).SetBytes(v)
}
return big.NewInt(0).SetBytes(ip.To16())
}
func intToIP(i *big.Int) net.IP {
return net.IP(i.Bytes())
}
// Network masks off the host portion of the IP
func Network(ipn *net.IPNet) *net.IPNet {
return &net.IPNet{
IP: ipn.IP.Mask(ipn.Mask),
Mask: ipn.Mask,
}
}

View File

@ -0,0 +1,27 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestIp(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Ip Suite")
}

View File

@ -0,0 +1,31 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"io/ioutil"
)
func EnableIP4Forward() error {
return echo1("/proc/sys/net/ipv4/ip_forward")
}
func EnableIP6Forward() error {
return echo1("/proc/sys/net/ipv6/conf/all/forwarding")
}
func echo1(f string) error {
return ioutil.WriteFile(f, []byte("1"), 0644)
}

View File

@ -0,0 +1,66 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"fmt"
"net"
"github.com/coreos/go-iptables/iptables"
)
// SetupIPMasq installs iptables rules to masquerade traffic
// coming from ipn and going outside of it
func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error {
ipt, err := iptables.New()
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
}
if err = ipt.NewChain("nat", chain); err != nil {
if err.(*iptables.Error).ExitStatus() != 1 {
// TODO(eyakubovich): assumes exit status 1 implies chain exists
return err
}
}
if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil {
return err
}
if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil {
return err
}
return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment)
}
// TeardownIPMasq undoes the effects of SetupIPMasq
func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error {
ipt, err := iptables.New()
if err != nil {
return fmt.Errorf("failed to locate iptables: %v", err)
}
if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment); err != nil {
return err
}
if err = ipt.ClearChain("nat", chain); err != nil {
return err
}
return ipt.DeleteChain("nat", chain)
}

View File

@ -0,0 +1,200 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"crypto/rand"
"fmt"
"net"
"os"
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/utils/hwaddr"
"github.com/vishvananda/netlink"
)
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
veth := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: name,
Flags: net.FlagUp,
MTU: mtu,
},
PeerName: peer,
}
if err := netlink.LinkAdd(veth); err != nil {
return nil, err
}
return veth, nil
}
func peerExists(name string) bool {
if _, err := netlink.LinkByName(name); err != nil {
return false
}
return true
}
func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
for i := 0; i < 10; i++ {
peerName, err = RandomVethName()
if err != nil {
return
}
veth, err = makeVethPair(name, peerName, mtu)
switch {
case err == nil:
return
case os.IsExist(err):
if peerExists(peerName) {
continue
}
err = fmt.Errorf("container veth name provided (%v) already exists", name)
return
default:
err = fmt.Errorf("failed to make veth pair: %v", err)
return
}
}
// should really never be hit
err = fmt.Errorf("failed to find a unique veth name")
return
}
// RandomVethName returns string "veth" with random prefix (hashed from entropy)
func RandomVethName() (string, error) {
entropy := make([]byte, 4)
_, err := rand.Reader.Read(entropy)
if err != nil {
return "", fmt.Errorf("failed to generate random veth name: %v", err)
}
// NetworkManager (recent versions) will ignore veth devices that start with "veth"
return fmt.Sprintf("veth%x", entropy), nil
}
func RenameLink(curName, newName string) error {
link, err := netlink.LinkByName(curName)
if err == nil {
err = netlink.LinkSetName(link, newName)
}
return err
}
// SetupVeth sets up a virtual ethernet link.
// Should be in container netns, and will switch back to hostNS to set the host
// veth end up.
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) {
var hostVethName string
hostVethName, contVeth, err = makeVeth(contVethName, mtu)
if err != nil {
return
}
if err = netlink.LinkSetUp(contVeth); err != nil {
err = fmt.Errorf("failed to set %q up: %v", contVethName, err)
return
}
hostVeth, err = netlink.LinkByName(hostVethName)
if err != nil {
err = fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
return
}
if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
err = fmt.Errorf("failed to move veth to host netns: %v", err)
return
}
err = hostNS.Do(func(_ ns.NetNS) error {
hostVeth, err = netlink.LinkByName(hostVethName)
if err != nil {
return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err)
}
if err = netlink.LinkSetUp(hostVeth); err != nil {
return fmt.Errorf("failed to set %q up: %v", hostVethName, err)
}
return nil
})
return
}
// DelLinkByName removes an interface link.
func DelLinkByName(ifName string) error {
iface, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
}
if err = netlink.LinkDel(iface); err != nil {
return fmt.Errorf("failed to delete %q: %v", ifName, err)
}
return nil
}
// DelLinkByNameAddr remove an interface returns its IP address
// of the specified family
func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
iface, err := netlink.LinkByName(ifName)
if err != nil {
return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
}
addrs, err := netlink.AddrList(iface, family)
if err != nil || len(addrs) == 0 {
return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
}
if err = netlink.LinkDel(iface); err != nil {
return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
}
return addrs[0].IPNet, nil
}
func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error {
iface, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
}
switch {
case ip4 == nil && ip6 == nil:
return fmt.Errorf("neither ip4 or ip6 specified")
case ip4 != nil:
{
hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix)
if err != nil {
return fmt.Errorf("failed to generate hardware addr: %v", err)
}
if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil {
return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err)
}
}
case ip6 != nil:
// TODO: IPv6
}
return nil
}

View File

@ -0,0 +1,259 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip_test
import (
"bytes"
"crypto/rand"
"fmt"
"net"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/ip"
"github.com/containernetworking/cni/pkg/ns"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
)
func getHwAddr(linkname string) string {
veth, err := netlink.LinkByName(linkname)
Expect(err).NotTo(HaveOccurred())
return fmt.Sprintf("%s", veth.Attrs().HardwareAddr)
}
var _ = Describe("Link", func() {
const (
ifaceFormatString string = "i%d"
mtu int = 1400
ip4onehwaddr = "0a:58:01:01:01:01"
)
var (
hostNetNS ns.NetNS
containerNetNS ns.NetNS
ifaceCounter int = 0
hostVeth netlink.Link
containerVeth netlink.Link
hostVethName string
containerVethName string
ip4one = net.ParseIP("1.1.1.1")
ip4two = net.ParseIP("1.1.1.2")
originalRandReader = rand.Reader
)
BeforeEach(func() {
var err error
hostNetNS, err = ns.NewNS()
Expect(err).NotTo(HaveOccurred())
containerNetNS, err = ns.NewNS()
Expect(err).NotTo(HaveOccurred())
fakeBytes := make([]byte, 20)
//to be reset in AfterEach block
rand.Reader = bytes.NewReader(fakeBytes)
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS)
if err != nil {
return err
}
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Attrs().Name
containerVethName = containerVeth.Attrs().Name
return nil
})
})
AfterEach(func() {
Expect(containerNetNS.Close()).To(Succeed())
Expect(hostNetNS.Close()).To(Succeed())
ifaceCounter++
rand.Reader = originalRandReader
})
It("SetupVeth must put the veth endpoints into the separate namespaces", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerVethFromName, err := netlink.LinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Attrs().Index))
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVethFromName, err := netlink.LinkByName(hostVethName)
Expect(err).NotTo(HaveOccurred())
Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Attrs().Index))
return nil
})
})
Context("when container already has an interface with the same name", func() {
It("returns useful error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName)))
return nil
})
})
})
Context("when there is no name available for the host-side", func() {
BeforeEach(func() {
//adding different interface to container ns
containerVethName += "0"
})
It("returns useful error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
Expect(err.Error()).To(Equal("failed to move veth to host netns: file exists"))
return nil
})
})
})
Context("when there is no name conflict for the host or container interfaces", func() {
BeforeEach(func() {
//adding different interface to container and host ns
containerVethName += "0"
rand.Reader = originalRandReader
})
It("successfully creates the second veth pair", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Attrs().Name
return nil
})
//verify veths are in different namespaces
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(hostVethName)
Expect(err).NotTo(HaveOccurred())
return nil
})
})
})
It("DelLinkByName must delete the veth endpoints", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// this will delete the host endpoint too
err := ip.DelLinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(containerVethName)
Expect(err).To(HaveOccurred())
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(hostVethName)
Expect(err).To(HaveOccurred())
return nil
})
})
It("DelLinkByNameAddr must throw an error for configured interfaces", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// this will delete the host endpoint too
addr, err := ip.DelLinkByNameAddr(containerVethName, nl.FAMILY_V4)
Expect(err).To(HaveOccurred())
var ipNetNil *net.IPNet
Expect(addr).To(Equal(ipNetNil))
return nil
})
})
It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
var err error
hwaddrBefore := getHwAddr(containerVethName)
err = ip.SetHWAddrByIP(containerVethName, ip4one, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter1 := getHwAddr(containerVethName)
Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1))
Expect(hwaddrAfter1).To(Equal(ip4onehwaddr))
return nil
})
})
It("SetHWAddrByIP must be injective", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
err := ip.SetHWAddrByIP(containerVethName, ip4one, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter1 := getHwAddr(containerVethName)
err = ip.SetHWAddrByIP(containerVethName, ip4two, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter2 := getHwAddr(containerVethName)
Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2))
return nil
})
})
})

View File

@ -0,0 +1,27 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"net"
"github.com/vishvananda/netlink"
)
// AddDefaultRoute sets the default route on the given gateway.
func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
_, defNet, _ := net.ParseCIDR("0.0.0.0/0")
return AddRoute(defNet, gw, dev)
}

View File

@ -0,0 +1,41 @@
// Copyright 2015-2017 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ip
import (
"net"
"github.com/vishvananda/netlink"
)
// AddRoute adds a universally-scoped route to a device.
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
return netlink.RouteAdd(&netlink.Route{
LinkIndex: dev.Attrs().Index,
Scope: netlink.SCOPE_UNIVERSE,
Dst: ipn,
Gw: gw,
})
}
// AddHostRoute adds a host-scoped route to a device.
func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
return netlink.RouteAdd(&netlink.Route{
LinkIndex: dev.Attrs().Index,
Scope: netlink.SCOPE_HOST,
Dst: ipn,
Gw: gw,
})
}

View File

@ -0,0 +1,34 @@
// Copyright 2015-2017 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.
// +build !linux
package ip
import (
"net"
"github.com/containernetworking/cni/pkg/types"
"github.com/vishvananda/netlink"
)
// AddRoute adds a universally-scoped route to a device.
func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
return types.NotImplementedError
}
// AddHostRoute adds a host-scoped route to a device.
func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
return types.NotImplementedError
}

View File

@ -0,0 +1,93 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ipam
import (
"fmt"
"net"
"os"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/ip"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/vishvananda/netlink"
)
func ExecAdd(plugin string, netconf []byte) (types.Result, error) {
return invoke.DelegateAdd(plugin, netconf)
}
func ExecDel(plugin string, netconf []byte) error {
return invoke.DelegateDel(plugin, netconf)
}
// ConfigureIface takes the result of IPAM plugin and
// applies to the ifName interface
func ConfigureIface(ifName string, res *current.Result) error {
if len(res.Interfaces) == 0 {
return fmt.Errorf("no interfaces to configure")
}
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)
}
var v4gw, v6gw net.IP
for _, ipc := range res.IPs {
if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName {
// IP address is for a different interface
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
}
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
if err = netlink.AddrAdd(link, addr); err != nil {
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
}
gwIsV4 := ipc.Gateway.To4() != nil
if gwIsV4 && v4gw == nil {
v4gw = ipc.Gateway
} else if !gwIsV4 && v6gw == nil {
v6gw = ipc.Gateway
}
}
for _, r := range res.Routes {
routeIsV4 := r.Dst.IP.To4() != nil
gw := r.GW
if gw == nil {
if routeIsV4 && v4gw != nil {
gw = v4gw
} else if !routeIsV4 && v6gw != nil {
gw = v6gw
}
}
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
}

View File

@ -0,0 +1,27 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ipam_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestIpam(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Ipam Suite")
}

View File

@ -0,0 +1,258 @@
// Copyright 2015 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ipam
import (
"net"
"syscall"
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/vishvananda/netlink"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const LINK_NAME = "eth0"
func ipNetEqual(a, b *net.IPNet) bool {
aPrefix, aBits := a.Mask.Size()
bPrefix, bBits := b.Mask.Size()
if aPrefix != bPrefix || aBits != bBits {
return false
}
return a.IP.Equal(b.IP)
}
var _ = Describe("IPAM Operations", func() {
var originalNS ns.NetNS
var ipv4, ipv6, routev4, routev6 *net.IPNet
var ipgw4, ipgw6, routegwv4, routegwv6 net.IP
var result *current.Result
BeforeEach(func() {
// Create a new NetNS so we don't modify the host
var err error
originalNS, err = ns.NewNS()
Expect(err).NotTo(HaveOccurred())
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// Add master
err = netlink.LinkAdd(&netlink.Dummy{
LinkAttrs: netlink.LinkAttrs{
Name: LINK_NAME,
},
})
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(LINK_NAME)
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
ipv4, err = types.ParseCIDR("1.2.3.30/24")
Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil())
_, routev4, err = net.ParseCIDR("15.5.6.8/24")
Expect(err).NotTo(HaveOccurred())
Expect(routev4).NotTo(BeNil())
routegwv4 = net.ParseIP("1.2.3.5")
Expect(routegwv4).NotTo(BeNil())
ipgw4 = net.ParseIP("1.2.3.1")
Expect(ipgw4).NotTo(BeNil())
ipv6, err = types.ParseCIDR("abcd:1234:ffff::cdde/64")
Expect(err).NotTo(HaveOccurred())
Expect(ipv6).NotTo(BeNil())
_, routev6, err = net.ParseCIDR("1111:dddd::aaaa/80")
Expect(err).NotTo(HaveOccurred())
Expect(routev6).NotTo(BeNil())
routegwv6 = net.ParseIP("abcd:1234:ffff::10")
Expect(routegwv6).NotTo(BeNil())
ipgw6 = net.ParseIP("abcd:1234:ffff::1")
Expect(ipgw6).NotTo(BeNil())
result = &current.Result{
Interfaces: []*current.Interface{
{
Name: "eth0",
Mac: "00:11:22:33:44:55",
Sandbox: "/proc/3553/ns/net",
},
{
Name: "fake0",
Mac: "00:33:44:55:66:77",
Sandbox: "/proc/1234/ns/net",
},
},
IPs: []*current.IPConfig{
{
Version: "4",
Interface: 0,
Address: *ipv4,
Gateway: ipgw4,
},
{
Version: "6",
Interface: 0,
Address: *ipv6,
Gateway: ipgw6,
},
},
Routes: []*types.Route{
{Dst: *routev4, GW: routegwv4},
{Dst: *routev6, GW: routegwv6},
},
}
})
AfterEach(func() {
Expect(originalNS.Close()).To(Succeed())
})
It("configures a link with addresses and routes", func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
err := ConfigureIface(LINK_NAME, result)
Expect(err).NotTo(HaveOccurred())
link, err := netlink.LinkByName(LINK_NAME)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
v4addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(v4addrs)).To(Equal(1))
Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true))
v6addrs, err := netlink.AddrList(link, syscall.AF_INET6)
Expect(err).NotTo(HaveOccurred())
Expect(len(v6addrs)).To(Equal(2))
var found bool
for _, a := range v6addrs {
if ipNetEqual(a.IPNet, ipv6) {
found = true
break
}
}
Expect(found).To(Equal(true))
// Ensure the v4 route, v6 route, and subnet route
routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred())
var v4found, v6found bool
for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) {
v4found = true
}
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) {
v6found = true
}
if v4found && v6found {
break
}
}
Expect(v4found).To(Equal(true))
Expect(v6found).To(Equal(true))
return nil
})
Expect(err).NotTo(HaveOccurred())
})
It("configures a link with routes using address gateways", func() {
result.Routes[0].GW = nil
result.Routes[1].GW = nil
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
err := ConfigureIface(LINK_NAME, result)
Expect(err).NotTo(HaveOccurred())
link, err := netlink.LinkByName(LINK_NAME)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(LINK_NAME))
// Ensure the v4 route, v6 route, and subnet route
routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred())
var v4found, v6found bool
for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
v4found = true
}
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) {
v6found = true
}
if v4found && v6found {
break
}
}
Expect(v4found).To(Equal(true))
Expect(v6found).To(Equal(true))
return nil
})
Expect(err).NotTo(HaveOccurred())
})
It("returns an error when the interface index doesn't match the link name", func() {
result.IPs[0].Interface = 1
err := originalNS.Do(func(ns.NetNS) error {
return ConfigureIface(LINK_NAME, result)
})
Expect(err).To(HaveOccurred())
})
It("returns an error when the interface index is too big", func() {
result.IPs[0].Interface = 2
err := originalNS.Do(func(ns.NetNS) error {
return ConfigureIface(LINK_NAME, result)
})
Expect(err).To(HaveOccurred())
})
It("returns an error when there are no interfaces to configure", func() {
result.Interfaces = []*current.Interface{}
err := originalNS.Do(func(ns.NetNS) error {
return ConfigureIface(LINK_NAME, result)
})
Expect(err).To(HaveOccurred())
})
It("returns an error when configuring the wrong interface", func() {
err := originalNS.Do(func(ns.NetNS) error {
return ConfigureIface("asdfasdf", result)
})
Expect(err).To(HaveOccurred())
})
})

View File

@ -0,0 +1,63 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hwaddr
import (
"fmt"
"net"
)
const (
ipRelevantByteLen = 4
PrivateMACPrefixString = "0a:58"
)
var (
// private mac prefix safe to use
PrivateMACPrefix = []byte{0x0a, 0x58}
)
type SupportIp4OnlyErr struct{ msg string }
func (e SupportIp4OnlyErr) Error() string { return e.msg }
type MacParseErr struct{ msg string }
func (e MacParseErr) Error() string { return e.msg }
type InvalidPrefixLengthErr struct{ msg string }
func (e InvalidPrefixLengthErr) Error() string { return e.msg }
// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input.
func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) {
switch {
case ip.To4() == nil:
return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"}
case len(prefix) != len(PrivateMACPrefix):
return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf(
"Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)),
}
}
ipByteLen := len(ip)
return (net.HardwareAddr)(
append(
prefix,
ip[ipByteLen-ipRelevantByteLen:ipByteLen]...),
), nil
}

View File

@ -0,0 +1,27 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hwaddr_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestHwaddr(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Hwaddr Suite")
}

View File

@ -0,0 +1,74 @@
// Copyright 2016 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hwaddr_test
import (
"net"
"github.com/containernetworking/cni/pkg/utils/hwaddr"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Hwaddr", func() {
Context("Generate Hardware Address", func() {
It("generate hardware address based on ipv4 address", func() {
testCases := []struct {
ip net.IP
expectedMAC net.HardwareAddr
}{
{
ip: net.ParseIP("10.0.0.2"),
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0x00, 0x00, 0x02)),
},
{
ip: net.ParseIP("10.250.0.244"),
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0xfa, 0x00, 0xf4)),
},
{
ip: net.ParseIP("172.17.0.2"),
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
},
{
ip: net.IPv4(byte(172), byte(17), byte(0), byte(2)),
expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)),
},
}
for _, tc := range testCases {
mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix)
Expect(err).NotTo(HaveOccurred())
Expect(mac).To(Equal(tc.expectedMAC))
}
})
It("return error if input is not ipv4 address", func() {
testCases := []net.IP{
net.ParseIP(""),
net.ParseIP("2001:db8:0:1:1:1:1:1"),
}
for _, tc := range testCases {
_, err := hwaddr.GenerateHardwareAddr4(tc, hwaddr.PrivateMACPrefix)
Expect(err).To(BeAssignableToTypeOf(hwaddr.SupportIp4OnlyErr{}))
}
})
It("return error if prefix is invalid", func() {
_, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), []byte{0x58})
Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{}))
})
})
})