add DHCP IPAM plugin
authorEugene Yakubovich <eugene.yakubovich@coreos.com>
Tue, 19 May 2015 19:02:41 +0000 (12:02 -0700)
committerEugene Yakubovich <eugene.yakubovich@coreos.com>
Thu, 21 May 2015 20:36:51 +0000 (13:36 -0700)
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.

15 files changed:
Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir.go [deleted file]
Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir_test.go [deleted file]
Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock.go [deleted file]
Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock_test.go [deleted file]
pkg/ns/ns.go
plugins/ipam/dhcp/daemon.go [new file with mode: 0644]
plugins/ipam/dhcp/lease.go [new file with mode: 0644]
plugins/ipam/dhcp/main.go [new file with mode: 0644]
plugins/ipam/dhcp/options.go [new file with mode: 0644]
plugins/ipam/dhcp/options_test.go [new file with mode: 0644]
plugins/main/bridge/bridge.go
plugins/main/ipvlan/ipvlan.go
plugins/main/macvlan/macvlan.go
plugins/main/veth/veth.go
scripts/priv-net-run.sh

diff --git a/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir.go b/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir.go
deleted file mode 100644 (file)
index 265a276..0000000
+++ /dev/null
@@ -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
-}
diff --git a/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir_test.go b/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/dir_test.go
deleted file mode 100644 (file)
index fb86626..0000000
+++ /dev/null
@@ -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)
-       }
-}
diff --git a/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock.go b/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock.go
deleted file mode 100644 (file)
index 768b421..0000000
+++ /dev/null
@@ -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
-}
diff --git a/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock_test.go b/Godeps/_workspace/src/github.com/coreos/rkt/pkg/lock/keylock_test.go
deleted file mode 100644 (file)
index 56cc9f1..0000000
+++ /dev/null
@@ -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
-}
index 82291f9..20548b9 100644 (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 {
diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go
new file mode 100644 (file)
index 0000000..f39c58e
--- /dev/null
@@ -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)
+}
diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go
new file mode 100644 (file)
index 0000000..7a6377e
--- /dev/null
@@ -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),
+       )
+}
diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go
new file mode 100644 (file)
index 0000000..ccfb198
--- /dev/null
@@ -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
+}
diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go
new file mode 100644 (file)
index 0000000..1064daa
--- /dev/null
@@ -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")
+}
diff --git a/plugins/ipam/dhcp/options_test.go b/plugins/ipam/dhcp/options_test.go
new file mode 100644 (file)
index 0000000..3b4f801
--- /dev/null
@@ -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)
+}
index 2515c0e..c30f2ca 100644 (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)
        })
 }
index 0d2ba7a..0f3656c 100644 (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)
        })
 }
index 31a17ea..f4b432d 100644 (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)
        })
 }
index aef901b..c736060 100644 (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
index 3d1c1ec..d96d31e 100755 (executable)
@@ -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 "$@"