add DHCP IPAM plugin

The plugin binary actually functions in two modes. The first mode
is a regular CNI plugin. The second mode (when stared with "daemon" arg)
runs a DHCP client daemon. When executed as a CNI plugin, it issues
an RPC request to the daemon for actual processing. The daemon is
required since a DHCP lease needs to be maintained by periodically
renewing it. One instance of the daemon can server arbitrary number
of containers/leases.
This commit is contained in:
Eugene Yakubovich 2015-05-19 12:02:41 -07:00
parent 7a8ee49891
commit c70320b5ed
15 changed files with 791 additions and 838 deletions

View File

@ -1,190 +0,0 @@
// Copyright 2014 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 lock implements simple locking primitives on a
// regular file or directory using flock
package lock
import (
"errors"
"syscall"
)
var (
ErrLocked = errors.New("file already locked")
ErrNotExist = errors.New("file does not exist")
ErrPermission = errors.New("permission denied")
ErrNotRegular = errors.New("not a regular file")
)
// FileLock represents a lock on a regular file or a directory
type FileLock struct {
path string
fd int
}
type LockType int
const (
Dir LockType = iota
RegFile
)
// TryExclusiveLock takes an exclusive lock without blocking.
// This is idempotent when the Lock already represents an exclusive lock,
// and tries promote a shared lock to exclusive atomically.
// It will return ErrLocked if any lock is already held.
func (l *FileLock) TryExclusiveLock() error {
err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB)
if err == syscall.EWOULDBLOCK {
err = ErrLocked
}
return err
}
// TryExclusiveLock takes an exclusive lock on a file/directory without blocking.
// It will return ErrLocked if any lock is already held on the file/directory.
func TryExclusiveLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.TryExclusiveLock()
if err != nil {
return nil, err
}
return l, err
}
// ExclusiveLock takes an exclusive lock.
// This is idempotent when the Lock already represents an exclusive lock,
// and promotes a shared lock to exclusive atomically.
// It will block if an exclusive lock is already held.
func (l *FileLock) ExclusiveLock() error {
return syscall.Flock(l.fd, syscall.LOCK_EX)
}
// ExclusiveLock takes an exclusive lock on a file/directory.
// It will block if an exclusive lock is already held on the file/directory.
func ExclusiveLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err == nil {
err = l.ExclusiveLock()
}
if err != nil {
return nil, err
}
return l, nil
}
// TrySharedLock takes a co-operative (shared) lock without blocking.
// This is idempotent when the Lock already represents a shared lock,
// and tries demote an exclusive lock to shared atomically.
// It will return ErrLocked if an exclusive lock already exists.
func (l *FileLock) TrySharedLock() error {
err := syscall.Flock(l.fd, syscall.LOCK_SH|syscall.LOCK_NB)
if err == syscall.EWOULDBLOCK {
err = ErrLocked
}
return err
}
// TrySharedLock takes a co-operative (shared) lock on a file/directory without blocking.
// It will return ErrLocked if an exclusive lock already exists on the file/directory.
func TrySharedLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.TrySharedLock()
if err != nil {
return nil, err
}
return l, nil
}
// SharedLock takes a co-operative (shared) lock on.
// This is idempotent when the Lock already represents a shared lock,
// and demotes an exclusive lock to shared atomically.
// It will block if an exclusive lock is already held.
func (l *FileLock) SharedLock() error {
return syscall.Flock(l.fd, syscall.LOCK_SH)
}
// SharedLock takes a co-operative (shared) lock on a file/directory.
// It will block if an exclusive lock is already held on the file/directory.
func SharedLock(path string, lockType LockType) (*FileLock, error) {
l, err := NewLock(path, lockType)
if err != nil {
return nil, err
}
err = l.SharedLock()
if err != nil {
return nil, err
}
return l, nil
}
// Unlock unlocks the lock
func (l *FileLock) Unlock() error {
return syscall.Flock(l.fd, syscall.LOCK_UN)
}
// Fd returns the lock's file descriptor, or an error if the lock is closed
func (l *FileLock) Fd() (int, error) {
var err error
if l.fd == -1 {
err = errors.New("lock closed")
}
return l.fd, err
}
// Close closes the lock which implicitly unlocks it as well
func (l *FileLock) Close() error {
fd := l.fd
l.fd = -1
return syscall.Close(fd)
}
// NewLock opens a new lock on a file without acquisition
func NewLock(path string, lockType LockType) (*FileLock, error) {
l := &FileLock{path: path, fd: -1}
mode := syscall.O_RDONLY | syscall.O_CLOEXEC
if lockType == Dir {
mode |= syscall.O_DIRECTORY
}
lfd, err := syscall.Open(l.path, mode, 0)
if err != nil {
if err == syscall.ENOENT {
err = ErrNotExist
} else if err == syscall.EACCES {
err = ErrPermission
}
return nil, err
}
l.fd = lfd
var stat syscall.Stat_t
err = syscall.Fstat(lfd, &stat)
if err != nil {
return nil, err
}
// Check if the file is a regular file
if lockType == RegFile && !(stat.Mode&syscall.S_IFMT == syscall.S_IFREG) {
return nil, ErrNotRegular
}
return l, nil
}

View File

@ -1,156 +0,0 @@
// Copyright 2014 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 lock
import (
"io/ioutil"
"os"
"testing"
)
func TestNewLock(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
f.Close()
l, err := NewLock(f.Name(), RegFile)
if err != nil {
t.Fatalf("error creating NewFileLock: %v", err)
}
l.Close()
d, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.Remove(d)
l, err = NewLock(d, Dir)
if err != nil {
t.Fatalf("error creating NewLock: %v", err)
}
err = l.Close()
if err != nil {
t.Fatalf("error unlocking lock: %v", err)
}
if err = os.Remove(d); err != nil {
t.Fatalf("error removing tmpdir: %v", err)
}
l, err = NewLock(d, Dir)
if err == nil {
t.Fatalf("expected error creating lock on nonexistent path")
}
}
func TestExclusiveLock(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.Remove(dir)
// Set up the initial exclusive lock
l, err := ExclusiveLock(dir, Dir)
if err != nil {
t.Fatalf("error creating lock: %v", err)
}
// reacquire the exclusive lock using the receiver interface
err = l.TryExclusiveLock()
if err != nil {
t.Fatalf("error reacquiring exclusive lock: %v", err)
}
// Now try another exclusive lock, should fail
_, err = TryExclusiveLock(dir, Dir)
if err == nil {
t.Fatalf("expected err trying exclusive lock")
}
// Unlock the original lock
err = l.Close()
if err != nil {
t.Fatalf("error closing lock: %v", err)
}
// Now another exclusive lock should succeed
_, err = TryExclusiveLock(dir, Dir)
if err != nil {
t.Fatalf("error creating lock: %v", err)
}
}
func TestSharedLock(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.Remove(dir)
// Set up the initial shared lock
l1, err := SharedLock(dir, Dir)
if err != nil {
t.Fatalf("error creating new shared lock: %v", err)
}
err = l1.TrySharedLock()
if err != nil {
t.Fatalf("error reacquiring shared lock: %v", err)
}
// Subsequent shared locks should succeed
l2, err := TrySharedLock(dir, Dir)
if err != nil {
t.Fatalf("error creating shared lock: %v", err)
}
l3, err := TrySharedLock(dir, Dir)
if err != nil {
t.Fatalf("error creating shared lock: %v", err)
}
// But an exclusive lock should fail
_, err = TryExclusiveLock(dir, Dir)
if err == nil {
t.Fatal("expected exclusive lock to fail")
}
// Close the locks
err = l1.Close()
if err != nil {
t.Fatalf("error closing lock: %v", err)
}
err = l2.Close()
if err != nil {
t.Fatalf("error closing lock: %v", err)
}
// Only unlock one of them
err = l3.Unlock()
if err != nil {
t.Fatalf("error unlocking lock: %v", err)
}
// Now try an exclusive lock, should succeed
_, err = TryExclusiveLock(dir, Dir)
if err != nil {
t.Fatalf("error creating lock: %v", err)
}
}

View File

@ -1,272 +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 lock
import (
"fmt"
"os"
"path/filepath"
"syscall"
)
const (
defaultDirPerm os.FileMode = 0660
defaultFilePerm os.FileMode = 0660
defaultLockRetries = 3
)
type keyLockMode uint
const (
keyLockExclusive keyLockMode = 1 << iota
keyLockShared
keyLockNonBlocking
)
// KeyLock is a lock for a specific key. The lock file is created inside a
// directory using the key name.
// This is useful when multiple processes want to take a lock but cannot use
// FileLock as they don't have a well defined file on the filesystem.
// key value must be a valid file name (as the lock file is named after the key
// value).
type KeyLock struct {
lockDir string
key string
// The lock on the key
keyLock *FileLock
}
// NewKeyLock returns a KeyLock for the specified key without acquisition.
// lockdir is the directory where the lock file will be created. If lockdir
// doesn't exists it will be created.
// key value must be a valid file name (as the lock file is named after the key
// value).
func NewKeyLock(lockDir string, key string) (*KeyLock, error) {
err := os.MkdirAll(lockDir, defaultDirPerm)
if err != nil {
return nil, err
}
keyLockFile := filepath.Join(lockDir, key)
// create the file if it doesn't exists
f, err := os.OpenFile(keyLockFile, os.O_RDONLY|os.O_CREATE, defaultFilePerm)
if err != nil {
return nil, fmt.Errorf("error creating key lock file: %v", err)
}
f.Close()
keyLock, err := NewLock(keyLockFile, RegFile)
if err != nil {
return nil, fmt.Errorf("error opening key lock file: %v", err)
}
return &KeyLock{lockDir: lockDir, key: key, keyLock: keyLock}, nil
}
// Close closes the key lock which implicitly unlocks it as well
func (l *KeyLock) Close() {
l.keyLock.Close()
}
// TryExclusiveLock takes an exclusive lock on a key without blocking.
// This is idempotent when the KeyLock already represents an exclusive lock,
// and tries promote a shared lock to exclusive atomically.
// It will return ErrLocked if any lock is already held on the key.
func (l *KeyLock) TryExclusiveKeyLock() error {
return l.lock(keyLockExclusive|keyLockNonBlocking, defaultLockRetries)
}
// TryExclusiveLock takes an exclusive lock on the key without blocking.
// lockDir is the directory where the lock file will be created.
// It will return ErrLocked if any lock is already held.
func TryExclusiveKeyLock(lockDir string, key string) (*KeyLock, error) {
return createAndLock(lockDir, key, keyLockExclusive|keyLockNonBlocking)
}
// ExclusiveLock takes an exclusive lock on a key.
// This is idempotent when the KeyLock already represents an exclusive lock,
// and promotes a shared lock to exclusive atomically.
// It will block if an exclusive lock is already held on the key.
func (l *KeyLock) ExclusiveKeyLock() error {
return l.lock(keyLockExclusive, defaultLockRetries)
}
// ExclusiveLock takes an exclusive lock on a key.
// lockDir is the directory where the lock file will be created.
// It will block if an exclusive lock is already held on the key.
func ExclusiveKeyLock(lockDir string, key string) (*KeyLock, error) {
return createAndLock(lockDir, key, keyLockExclusive)
}
// TrySharedLock takes a co-operative (shared) lock on the key without blocking.
// This is idempotent when the KeyLock already represents a shared lock,
// and tries demote an exclusive lock to shared atomically.
// It will return ErrLocked if an exclusive lock already exists on the key.
func (l *KeyLock) TrySharedKeyLock() error {
return l.lock(keyLockShared|keyLockNonBlocking, defaultLockRetries)
}
// TrySharedLock takes a co-operative (shared) lock on a key without blocking.
// lockDir is the directory where the lock file will be created.
// It will return ErrLocked if an exclusive lock already exists on the key.
func TrySharedKeyLock(lockDir string, key string) (*KeyLock, error) {
return createAndLock(lockDir, key, keyLockShared|keyLockNonBlocking)
}
// SharedLock takes a co-operative (shared) lock on a key.
// This is idempotent when the KeyLock already represents a shared lock,
// and demotes an exclusive lock to shared atomically.
// It will block if an exclusive lock is already held on the key.
func (l *KeyLock) SharedKeyLock() error {
return l.lock(keyLockShared, defaultLockRetries)
}
// SharedLock takes a co-operative (shared) lock on a key.
// lockDir is the directory where the lock file will be created.
// It will block if an exclusive lock is already held on the key.
func SharedKeyLock(lockDir string, key string) (*KeyLock, error) {
return createAndLock(lockDir, key, keyLockShared)
}
func createAndLock(lockDir string, key string, mode keyLockMode) (*KeyLock, error) {
keyLock, err := NewKeyLock(lockDir, key)
if err != nil {
return nil, err
}
err = keyLock.lock(mode, defaultLockRetries)
if err != nil {
return nil, err
}
return keyLock, nil
}
// lock is the base function to take a lock and handle changed lock files
// As there's the need to remove unused (see CleanKeyLocks) lock files without
// races, a changed file detection is needed.
//
// Without changed file detection this can happen:
//
// Process A takes exclusive lock on file01
// Process B waits for exclusive lock on file01.
// Process A deletes file01 and then releases the lock.
// Process B takes the lock on the removed file01 as it has the fd opened
// Process C comes, creates the file as it doesn't exists, and it also takes an exclusive lock.
// Now B and C thinks to own an exclusive lock.
//
// maxRetries can be passed, useful for testing.
func (l *KeyLock) lock(mode keyLockMode, maxRetries int) error {
retries := 0
for {
var err error
var isExclusive bool
var isNonBlocking bool
if mode&keyLockExclusive != 0 {
isExclusive = true
}
if mode&keyLockNonBlocking != 0 {
isNonBlocking = true
}
switch {
case isExclusive && !isNonBlocking:
err = l.keyLock.ExclusiveLock()
case isExclusive && isNonBlocking:
err = l.keyLock.TryExclusiveLock()
case !isExclusive && !isNonBlocking:
err = l.keyLock.SharedLock()
case !isExclusive && isNonBlocking:
err = l.keyLock.TrySharedLock()
}
if err != nil {
return err
}
// Check that the file referenced by the lock fd is the same as
// the current file on the filesystem
var lockStat, curStat syscall.Stat_t
lfd, err := l.keyLock.Fd()
if err != nil {
return err
}
err = syscall.Fstat(lfd, &lockStat)
if err != nil {
return err
}
keyLockFile := filepath.Join(l.lockDir, l.key)
fd, err := syscall.Open(keyLockFile, syscall.O_RDONLY, 0)
// If there's an error opening the file return an error
if err != nil {
return err
}
err = syscall.Fstat(fd, &curStat)
if err != nil {
return err
}
if lockStat.Ino == curStat.Ino && lockStat.Dev == curStat.Dev {
return nil
}
if retries >= maxRetries {
return fmt.Errorf("cannot acquire lock after %d retries", retries)
}
// If the file has changed discard this lock and try to take another lock.
l.keyLock.Close()
nl, err := NewKeyLock(l.lockDir, l.key)
if err != nil {
return err
}
l.keyLock = nl.keyLock
retries++
}
}
// Unlock unlocks the key lock.
func (l *KeyLock) Unlock() error {
err := l.keyLock.Unlock()
if err != nil {
return err
}
return nil
}
// CleanKeyLocks remove lock files from the lockDir.
// For every key it tries to take an Exclusive lock on it and skip it if it
// fails with ErrLocked
func CleanKeyLocks(lockDir string) error {
f, err := os.Open(lockDir)
if err != nil {
return fmt.Errorf("error opening lockDir: %v", err)
}
defer f.Close()
files, err := f.Readdir(0)
if err != nil {
return fmt.Errorf("error getting lock files list: %v", err)
}
for _, f := range files {
filename := filepath.Join(lockDir, f.Name())
keyLock, err := TryExclusiveKeyLock(lockDir, f.Name())
if err == ErrLocked {
continue
}
if err != nil {
return err
}
err = os.Remove(filename)
if err != nil {
keyLock.Close()
return fmt.Errorf("error removing lock file: %v", err)
}
keyLock.Close()
}
return nil
}

View File

@ -1,203 +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 lock
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestExclusiveKeyLock(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.RemoveAll(dir)
l1, err := ExclusiveKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating key lock: %v", err)
}
_, err = TryExclusiveKeyLock(dir, "key01")
if err == nil {
t.Fatalf("expected err trying exclusive key lock")
}
l1.Close()
}
func TestCleanKeyLocks(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.RemoveAll(dir)
l1, err := ExclusiveKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
err = CleanKeyLocks(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
filesnum, err := countFiles(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if filesnum != 1 {
t.Fatalf("expected 1 file in lock dir. found %d files", filesnum)
}
l2, err := SharedKeyLock(dir, "key02")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
l1.Close()
l2.Close()
err = CleanKeyLocks(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
filesnum, err = countFiles(dir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if filesnum != 0 {
t.Fatalf("expected empty lock dir. found %d files", filesnum)
}
}
func TestFileChangedLock(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.RemoveAll(dir)
l1, err := ExclusiveKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
l2, err := NewKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
// Simulate that l1 owner removes the actual key1 lock file
err = os.Remove(filepath.Join(dir, "key01"))
if err != nil {
t.Fatalf("error creating NewLock: %v", err)
}
l1.Close()
// Now l2 owner takes a lock, using the fd of the removed file
err = l2.lock(keyLockShared, 0)
if err == nil {
t.Fatalf("expected error")
}
l2.Close()
// Do the same with a new file created after removal
dir, err = ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.RemoveAll(dir)
l1, err = ExclusiveKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
l2, err = NewKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
// Simulate that l1 owner removes the actual key1 lock file
err = os.Remove(filepath.Join(dir, "key01"))
if err != nil {
t.Fatalf("error creating NewLock: %v", err)
}
l1.Close()
// Simulate that another user comes and takes a lock, this will create
// a new lock file as it was removed.
l3, err := ExclusiveKeyLock(dir, "key01")
l3.Close()
// Now l2 owner takes a lock, using the fd of the old file
err = l2.lock(keyLockShared, 0)
if err == nil {
t.Fatalf("expected error")
}
// Do the same but with a retry so if should work.
dir, err = ioutil.TempDir("", "")
if err != nil {
t.Fatalf("error creating tmpdir: %v", err)
}
defer os.RemoveAll(dir)
l1, err = ExclusiveKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
l2, err = NewKeyLock(dir, "key01")
if err != nil {
t.Fatalf("error creating keyLock: %v", err)
}
// Simulate that l1 owner removes the actual key1 lock file
err = os.Remove(filepath.Join(dir, "key01"))
if err != nil {
t.Fatalf("error creating NewLock: %v", err)
}
l1.Close()
// Simulate that another user comes and takes a lock, this will create
// a new lock file as it was removed.
l3, err = ExclusiveKeyLock(dir, "key01")
l3.Close()
// Now l2 owner takes a lock, using the fd of the old file
err = l2.lock(keyLockShared, 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func countFiles(dir string) (int, error) {
f, err := os.Open(dir)
if err != nil {
return -1, err
}
defer f.Close()
files, err := f.Readdir(0)
if err != nil {
return -1, err
}
return len(files), nil
}

View File

@ -48,19 +48,30 @@ func SetNS(f *os.File, flags uintptr) error {
// WithNetNSPath executes the passed closure under the given network
// namespace, restoring the original namespace afterwards.
func WithNetNSPath(nspath string, f func(*os.File) error) error {
// Changing namespaces must be done on a goroutine that has been
// locked to an OS thread. If lockThread arg is true, this function
// locks the goroutine prior to change namespace and unlocks before
// returning
func WithNetNSPath(nspath string, lockThread bool, f func(*os.File) error) error {
ns, err := os.Open(nspath)
if err != nil {
return fmt.Errorf("Failed to open %v: %v", nspath, err)
}
defer ns.Close()
return WithNetNS(ns, f)
return WithNetNS(ns, lockThread, f)
}
// WithNetNS executes the passed closure under the given network
// namespace, restoring the original namespace afterwards.
func WithNetNS(ns *os.File, f func(*os.File) error) error {
// Changing namespaces must be done on a goroutine that has been
// locked to an OS thread. If lockThread arg is true, this function
// locks the goroutine prior to change namespace and unlocks before
// returning
func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error {
if lockThread {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
}
// save a handle to current (host) network namespace
thisNS, err := os.Open("/proc/self/ns/net")
if err != nil {

157
plugins/ipam/dhcp/daemon.go Normal file
View File

@ -0,0 +1,157 @@
// 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 main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/appc/cni/Godeps/_workspace/src/github.com/coreos/go-systemd/activation"
"github.com/appc/cni/pkg/plugin"
"github.com/appc/cni/pkg/skel"
)
const listenFdsStart = 3
const resendCount = 3
var errNoMoreTries = errors.New("no more tries")
type DHCP struct {
mux sync.Mutex
leases map[string]*DHCPLease
}
func newDHCP() *DHCP {
return &DHCP{
leases: make(map[string]*DHCPLease),
}
}
// Allocate acquires an IP from a DHCP server for a specified container.
// The acquired lease will be maintained until Release() is called.
func (d *DHCP) Allocate(args *skel.CmdArgs, result *plugin.Result) error {
conf := plugin.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("error parsing netconf: %v", err)
}
clientID := args.ContainerID + "/" + conf.Name
l, err := AcquireLease(clientID, args.Netns, args.IfName)
if err != nil {
return err
}
ipn, err := l.IPNet()
if err != nil {
l.Stop()
return err
}
d.setLease(args.ContainerID, conf.Name, l)
result.IP4 = &plugin.IPConfig{
IP: *ipn,
Gateway: l.Gateway(),
Routes: l.Routes(),
}
return nil
}
// Release stops maintenance of the lease acquired in Allocate()
// and sends a release msg to the DHCP server.
func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
conf := plugin.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
return fmt.Errorf("error parsing netconf: %v", err)
}
if l := d.getLease(args.ContainerID, conf.Name); l != nil {
l.Stop()
return nil
}
return fmt.Errorf("lease not found: %v/%v", args.ContainerID, conf.Name)
}
func (d *DHCP) getLease(contID, netName string) *DHCPLease {
d.mux.Lock()
defer d.mux.Unlock()
// TODO(eyakubovich): hash it to avoid collisions
l, ok := d.leases[contID+netName]
if !ok {
return nil
}
return l
}
func (d *DHCP) setLease(contID, netName string, l *DHCPLease) {
d.mux.Lock()
defer d.mux.Unlock()
// TODO(eyakubovich): hash it to avoid collisions
d.leases[contID+netName] = l
}
func getListener() (net.Listener, error) {
l, err := activation.Listeners(true)
if err != nil {
return nil, err
}
switch {
case len(l) == 0:
if err := os.MkdirAll(filepath.Dir(socketPath), 0700); err != nil {
return nil, err
}
return net.Listen("unix", socketPath)
case len(l) == 1:
if l[0] == nil {
return nil, fmt.Errorf("LISTEN_FDS=1 but no FD found")
}
return l[0], nil
default:
return nil, fmt.Errorf("Too many (%v) FDs passed through socket activation", len(l))
}
}
func runDaemon() {
// since other goroutines (on separate threads) will change namespaces,
// ensure the RPC server does not get scheduled onto those
runtime.LockOSThread()
l, err := getListener()
if err != nil {
log.Printf("Error getting listener: %v", err)
return
}
dhcp := newDHCP()
rpc.Register(dhcp)
rpc.HandleHTTP()
http.Serve(l, nil)
}

329
plugins/ipam/dhcp/lease.go Normal file
View File

@ -0,0 +1,329 @@
// 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 main
import (
"fmt"
"log"
"math/rand"
"net"
"os"
"sync"
"time"
"github.com/appc/cni/Godeps/_workspace/src/github.com/d2g/dhcp4"
"github.com/appc/cni/Godeps/_workspace/src/github.com/d2g/dhcp4client"
"github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink"
"github.com/appc/cni/pkg/ns"
"github.com/appc/cni/pkg/plugin"
)
// RFC 2131 suggests using exponential backoff, starting with 4sec
// and randomized to +/- 1sec
const resendDelay0 = 4 * time.Second
const (
leaseStateBound = iota
leaseStateRenewing
leaseStateRebinding
)
// This implementation uses 1 OS thread per lease. This is because
// all the network operations have to be done in network namespace
// of the interface. This can be improved by switching to the proper
// namespace for network ops and using fewer threads. However, this
// needs to be done carefully as dhcp4client ops are blocking.
type DHCPLease struct {
clientID string
ack *dhcp4.Packet
opts dhcp4.Options
link netlink.Link
renewalTime time.Time
rebindingTime time.Time
expireTime time.Time
stop chan struct{}
wg sync.WaitGroup
}
// AcquireLease gets an DHCP lease and then maintains it in the background
// by periodically renewing it. The acquired lease can be released by
// calling DHCPLease.Stop()
func AcquireLease(clientID, netns, ifName string) (*DHCPLease, error) {
errCh := make(chan error, 1)
l := &DHCPLease{
clientID: clientID,
stop: make(chan struct{}),
}
log.Printf("%v: acquiring lease", clientID)
l.wg.Add(1)
go ns.WithNetNSPath(netns, true, func(_ *os.File) (e error) {
defer l.wg.Done()
link, err := netlink.LinkByName(ifName)
if err != nil {
errCh <- fmt.Errorf("error looking up %q", ifName)
return
}
l.link = link
if err = l.acquire(); err != nil {
errCh <- err
return
}
log.Printf("%v: lease acquired, expiration is %v", l.clientID, l.expireTime)
errCh <- nil
l.maintain()
return
})
err := <-errCh
if err != nil {
return nil, err
}
return l, nil
}
// Stop terminates the background task that maintains the lease
// and issues a DHCP Release
func (l *DHCPLease) Stop() {
close(l.stop)
l.wg.Wait()
}
func (l *DHCPLease) acquire() error {
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
ok, ack, err := c.Request()
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server NACK'd own offer")
default:
return &ack, nil
}
})
if err != nil {
return err
}
return l.commit(pkt)
}
func (l *DHCPLease) commit(ack *dhcp4.Packet) error {
opts := ack.ParseOptions()
leaseTime, err := parseLeaseTime(opts)
if err != nil {
return err
}
rebindingTime, err := parseRebindingTime(opts)
if err != nil || rebindingTime > leaseTime {
// Per RFC 2131 Section 4.4.5, it should default to 85% of lease time
rebindingTime = leaseTime * 85 / 100
}
renewalTime, err := parseRenewalTime(opts)
if err != nil || renewalTime > rebindingTime {
// Per RFC 2131 Section 4.4.5, it should default to 50% of lease time
renewalTime = leaseTime / 2
}
now := time.Now()
l.expireTime = now.Add(leaseTime)
l.renewalTime = now.Add(renewalTime)
l.rebindingTime = now.Add(rebindingTime)
l.ack = ack
l.opts = opts
return nil
}
func (l *DHCPLease) maintain() {
state := leaseStateBound
for {
var sleepDur time.Duration
switch state {
case leaseStateBound:
sleepDur = l.renewalTime.Sub(time.Now())
if sleepDur <= 0 {
log.Printf("%v: renewing lease", l.clientID)
state = leaseStateRenewing
continue
}
case leaseStateRenewing:
if err := l.renew(); err != nil {
log.Printf("%v: %v", l.clientID, err)
if time.Now().After(l.rebindingTime) {
log.Printf("%v: renawal time expired, rebinding", l.clientID)
state = leaseStateRebinding
}
} else {
log.Printf("%v: lease renewed, expiration is %v", l.clientID, l.expireTime)
state = leaseStateBound
}
case leaseStateRebinding:
if err := l.acquire(); err != nil {
log.Printf("%v: %v", l.clientID, err)
if time.Now().After(l.expireTime) {
log.Printf("%v: lease expired, bringing interface DOWN", l.clientID)
l.downIface()
return
}
} else {
log.Printf("%v: lease rebound, expiration is %v", l.clientID, l.expireTime)
state = leaseStateBound
}
}
select {
case <-time.After(sleepDur):
case <-l.stop:
if err := l.release(); err != nil {
log.Printf("%v: failed to release DHCP lease: %v", l.clientID, err)
}
return
}
}
}
func (l *DHCPLease) downIface() {
if err := netlink.LinkSetDown(l.link); err != nil {
log.Printf("%v: failed to bring %v interface DOWN: %v", l.clientID, l.link.Attrs().Name, err)
}
}
func (l *DHCPLease) renew() error {
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
pkt, err := backoffRetry(func() (*dhcp4.Packet, error) {
ok, ack, err := c.Renew(*l.ack)
switch {
case err != nil:
return nil, err
case !ok:
return nil, fmt.Errorf("DHCP server did not renew lease")
default:
return &ack, nil
}
})
if err != nil {
return err
}
l.commit(pkt)
return nil
}
func (l *DHCPLease) release() error {
log.Printf("%v: releasing lease", l.clientID)
c, err := newDHCPClient(l.link)
if err != nil {
return err
}
defer c.Close()
if err = c.Release(*l.ack); err != nil {
return fmt.Errorf("failed to send DHCPRELEASE")
}
return nil
}
func (l *DHCPLease) IPNet() (*net.IPNet, error) {
mask := parseSubnetMask(l.opts)
if mask == nil {
return nil, fmt.Errorf("DHCP option Subnet Mask not found in DHCPACK")
}
return &net.IPNet{
IP: l.ack.YIAddr(),
Mask: mask,
}, nil
}
func (l *DHCPLease) Gateway() net.IP {
return parseRouter(l.opts)
}
func (l *DHCPLease) Routes() []plugin.Route {
routes := parseRoutes(l.opts)
return append(routes, parseCIDRRoutes(l.opts)...)
}
// jitter returns a random value within [-span, span) range
func jitter(span time.Duration) time.Duration {
return time.Duration(float64(span) * (2.0*rand.Float64() - 1.0))
}
func backoffRetry(f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
var baseDelay time.Duration = resendDelay0
for i := 0; i < resendCount; i++ {
pkt, err := f()
if err == nil {
return pkt, nil
}
log.Print(err)
time.Sleep(baseDelay + jitter(time.Second))
baseDelay *= 2
}
return nil, errNoMoreTries
}
func newDHCPClient(link netlink.Link) (*dhcp4client.Client, error) {
pktsock, err := dhcp4client.NewPacketSock(link.Attrs().Index)
if err != nil {
return nil, err
}
return dhcp4client.New(
dhcp4client.HardwareAddr(link.Attrs().HardwareAddr),
dhcp4client.Timeout(10*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(pktsock),
)
}

64
plugins/ipam/dhcp/main.go Normal file
View File

@ -0,0 +1,64 @@
// 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 main
import (
"fmt"
"net/rpc"
"os"
"github.com/appc/cni/pkg/plugin"
"github.com/appc/cni/pkg/skel"
)
const socketPath = "/run/cni/dhcp.sock"
func main() {
if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon()
} else {
skel.PluginMain(cmdAdd, cmdDel)
}
}
func cmdAdd(args *skel.CmdArgs) error {
client, err := rpc.DialHTTP("unix", socketPath)
if err != nil {
return fmt.Errorf("error dialing DHCP daemon: %v", err)
}
result := &plugin.Result{}
err = client.Call("DHCP.Allocate", args, result)
if err != nil {
return fmt.Errorf("error calling DHCP.Add: %v", err)
}
return plugin.PrintResult(result)
}
func cmdDel(args *skel.CmdArgs) error {
client, err := rpc.DialHTTP("unix", socketPath)
if err != nil {
return fmt.Errorf("error dialing DHCP daemon: %v", err)
}
dummy := struct{}{}
err = client.Call("DHCP.Release", args, &dummy)
if err != nil {
return fmt.Errorf("error calling DHCP.Del: %v", err)
}
return nil
}

View File

@ -0,0 +1,139 @@
// 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 main
import (
"encoding/binary"
"fmt"
"net"
"time"
"github.com/appc/cni/Godeps/_workspace/src/github.com/d2g/dhcp4"
"github.com/appc/cni/pkg/plugin"
)
func parseRouter(opts dhcp4.Options) net.IP {
if opts, ok := opts[dhcp4.OptionRouter]; ok {
if len(opts) == 4 {
return net.IP(opts)
}
}
return nil
}
func classfulSubnet(sn net.IP) net.IPNet {
return net.IPNet{
IP: sn,
Mask: sn.DefaultMask(),
}
}
func parseRoutes(opts dhcp4.Options) []plugin.Route {
// StaticRoutes format: pairs of:
// Dest = 4 bytes; Classful IP subnet
// Router = 4 bytes; IP address of router
routes := []plugin.Route{}
if opt, ok := opts[dhcp4.OptionStaticRoute]; ok {
for len(opt) >= 8 {
sn := opt[0:4]
r := opt[4:8]
rt := plugin.Route{
Dst: classfulSubnet(sn),
GW: r,
}
routes = append(routes, rt)
opt = opt[8:]
}
}
return routes
}
func parseCIDRRoutes(opts dhcp4.Options) []plugin.Route {
// See RFC4332 for format (http://tools.ietf.org/html/rfc3442)
routes := []plugin.Route{}
if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok {
for len(opt) >= 5 {
width := int(opt[0])
if width > 32 {
// error: can't have more than /32
return nil
}
// network bits are compacted to avoid zeros
octets := 0
if width > 0 {
octets = (width-1)/8 + 1
}
if len(opt) < 1+octets+4 {
// error: too short
return nil
}
sn := make([]byte, 4)
copy(sn, opt[1:octets+1])
gw := net.IP(opt[octets+1 : octets+5])
rt := plugin.Route{
Dst: net.IPNet{
IP: net.IP(sn),
Mask: net.CIDRMask(width, 32),
},
GW: gw,
}
routes = append(routes, rt)
opt = opt[octets+5 : len(opt)]
}
}
return routes
}
func parseSubnetMask(opts dhcp4.Options) net.IPMask {
mask, ok := opts[dhcp4.OptionSubnetMask]
if !ok {
return nil
}
return net.IPMask(mask)
}
func parseDuration(opts dhcp4.Options, code dhcp4.OptionCode, optName string) (time.Duration, error) {
val, ok := opts[code]
if !ok {
return 0, fmt.Errorf("option %v not found", optName)
}
if len(val) != 4 {
return 0, fmt.Errorf("option %v is not 4 bytes", optName)
}
secs := binary.BigEndian.Uint32(val)
return time.Duration(secs) * time.Second, nil
}
func parseLeaseTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionIPAddressLeaseTime, "LeaseTime")
}
func parseRenewalTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRenewalTimeValue, "RenewalTime")
}
func parseRebindingTime(opts dhcp4.Options) (time.Duration, error) {
return parseDuration(opts, dhcp4.OptionRebindingTimeValue, "RebindingTime")
}

View File

@ -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 main
import (
"net"
"testing"
"github.com/appc/cni/Godeps/_workspace/src/github.com/d2g/dhcp4"
"github.com/appc/cni/pkg/plugin"
)
func validateRoutes(t *testing.T, routes []plugin.Route) {
expected := []plugin.Route{
plugin.Route{
Dst: net.IPNet{
IP: net.IPv4(10, 0, 0, 0),
Mask: net.CIDRMask(8, 32),
},
GW: net.IPv4(10, 1, 2, 3),
},
plugin.Route{
Dst: net.IPNet{
IP: net.IPv4(192, 168, 1, 0),
Mask: net.CIDRMask(24, 32),
},
GW: net.IPv4(192, 168, 2, 3),
},
}
if len(routes) != len(expected) {
t.Fatalf("wrong length slice; expected %v, got %v", len(expected), len(routes))
}
for i := 0; i < len(routes); i++ {
a := routes[i]
e := expected[i]
if a.Dst.String() != e.Dst.String() {
t.Errorf("route.Dst mismatch: expected %v, got %v", e.Dst, a.Dst)
}
if !a.GW.Equal(e.GW) {
t.Errorf("route.GW mismatch: expected %v, got %v", e.GW, a.GW)
}
}
}
func TestParseRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionStaticRoute] = []byte{10, 0, 0, 0, 10, 1, 2, 3, 192, 168, 1, 0, 192, 168, 2, 3}
routes := parseRoutes(opts)
validateRoutes(t, routes)
}
func TestParseCIDRRoutes(t *testing.T) {
opts := make(dhcp4.Options)
opts[dhcp4.OptionClasslessRouteFormat] = []byte{8, 10, 10, 1, 2, 3, 24, 192, 168, 1, 192, 168, 2, 3}
routes := parseCIDRRoutes(opts)
validateRoutes(t, routes)
}

View File

@ -124,7 +124,7 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
func setupVeth(netns string, br *netlink.Bridge, ifName string, mtu int) error {
var hostVethName string
err := ns.WithNetNSPath(netns, func(hostNS *os.File) error {
err := ns.WithNetNSPath(netns, false, func(hostNS *os.File) error {
// create the veth pair in the container and move host end into host netns
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil {
@ -196,7 +196,7 @@ func cmdAdd(args *skel.CmdArgs) error {
result.IP4.Gateway = calcGatewayIP(&result.IP4.IP)
}
err = ns.WithNetNSPath(args.Netns, func(hostNS *os.File) error {
err = ns.WithNetNSPath(args.Netns, false, func(hostNS *os.File) error {
return plugin.ConfigureIface(args.IfName, result)
})
if err != nil {
@ -235,7 +235,7 @@ func cmdDel(args *skel.CmdArgs) error {
return err
}
return ns.WithNetNSPath(args.Netns, func(hostNS *os.File) error {
return ns.WithNetNSPath(args.Netns, false, func(hostNS *os.File) error {
return ip.DelLinkByName(args.IfName)
})
}

View File

@ -97,7 +97,7 @@ func createIpvlan(conf *NetConf, ifName string, netns *os.File) error {
return fmt.Errorf("failed to create ipvlan: %v", err)
}
return ns.WithNetNS(netns, func(_ *os.File) error {
return ns.WithNetNS(netns, false, func(_ *os.File) error {
err := renameLink(tmpName, ifName)
if err != nil {
return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err)
@ -131,7 +131,7 @@ func cmdAdd(args *skel.CmdArgs) error {
return errors.New("IPAM plugin returned missing IPv4 config")
}
err = ns.WithNetNS(netns, func(_ *os.File) error {
err = ns.WithNetNS(netns, false, func(_ *os.File) error {
return plugin.ConfigureIface(args.IfName, result)
})
if err != nil {
@ -159,7 +159,7 @@ func cmdDel(args *skel.CmdArgs) error {
return err
}
return ns.WithNetNSPath(args.Netns, func(hostNS *os.File) error {
return ns.WithNetNSPath(args.Netns, false, func(hostNS *os.File) error {
return ip.DelLinkByName(args.IfName)
})
}

View File

@ -101,7 +101,7 @@ func createMacvlan(conf *NetConf, ifName string, netns *os.File) error {
return fmt.Errorf("failed to create macvlan: %v", err)
}
return ns.WithNetNS(netns, func(_ *os.File) error {
return ns.WithNetNS(netns, false, func(_ *os.File) error {
err := renameLink(tmpName, ifName)
if err != nil {
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
@ -135,7 +135,7 @@ func cmdAdd(args *skel.CmdArgs) error {
return errors.New("IPAM plugin returned missing IPv4 config")
}
err = ns.WithNetNS(netns, func(_ *os.File) error {
err = ns.WithNetNS(netns, false, func(_ *os.File) error {
return plugin.ConfigureIface(args.IfName, result)
})
if err != nil {
@ -163,7 +163,7 @@ func cmdDel(args *skel.CmdArgs) error {
return err
}
return ns.WithNetNSPath(args.Netns, func(hostNS *os.File) error {
return ns.WithNetNSPath(args.Netns, false, func(hostNS *os.File) error {
return ip.DelLinkByName(args.IfName)
})
}

View File

@ -46,7 +46,7 @@ type NetConf struct {
func setupContainerVeth(netns, ifName string, mtu int, pr *plugin.Result) (string, error) {
var hostVethName string
err := ns.WithNetNSPath(netns, func(hostNS *os.File) error {
err := ns.WithNetNSPath(netns, false, func(hostNS *os.File) error {
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil {
return err
@ -131,7 +131,7 @@ func cmdDel(args *skel.CmdArgs) error {
}
var ipn *net.IPNet
err := ns.WithNetNSPath(args.Netns, func(hostNS *os.File) error {
err := ns.WithNetNSPath(args.Netns, false, func(hostNS *os.File) error {
var err error
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
return err

View File

@ -2,7 +2,6 @@
# Run a command in a private network namespace
# set up by CNI plugins
contid=$(printf '%x%x%x%x' $RANDOM $RANDOM $RANDOM $RANDOM)
netnspath=/var/run/netns/$contid
@ -17,4 +16,4 @@ function cleanup() {
}
trap cleanup EXIT
ip netns exec $contid $@
ip netns exec $contid "$@"