host-local: allow ip request via CNI_ARGS
authorStefan Junker <mail@stefanjunker.de>
Sun, 16 Aug 2015 00:30:04 +0000 (02:30 +0200)
committerStefan Junker <mail@stefanjunker.de>
Thu, 3 Sep 2015 23:38:22 +0000 (01:38 +0200)
A specific IP can now be requested via the environment variable CNI_ARGS, e.g.
`CNI_ARGS=ip=1.2.3.4`.
The plugin will try to reserve the specified IP.
If this is not successful the execution will fail.

Documentation/host-local.md
pkg/plugin/args.go [new file with mode: 0644]
plugins/ipam/host-local/allocator.go
plugins/ipam/host-local/config.go
plugins/ipam/host-local/main.go

index ff6ee77..35aba35 100644 (file)
@@ -31,6 +31,11 @@ It stores the state locally on the host filesystem, therefore ensuring uniquenes
 * `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
 * `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
 
+## Supported arguments
+The following [CNI_ARGS](https://github.com/appc/cni/blob/master/SPEC.md#parameters) are supported:
+
+* `ip`: request a specific IP address from the subnet. If it's not available, the plugin will exit with an error
+
 ## Files
 
 Allocated IP addresses are stored as files in /var/lib/cni/networks/$NETWORK_NAME.
diff --git a/pkg/plugin/args.go b/pkg/plugin/args.go
new file mode 100644 (file)
index 0000000..8489579
--- /dev/null
@@ -0,0 +1,36 @@
+package plugin
+
+import (
+       "encoding"
+       "fmt"
+       "reflect"
+       "strings"
+)
+
+func LoadArgs(args string, container interface{}) error {
+       if args == "" {
+               return nil
+       }
+
+       containerValue := reflect.ValueOf(container)
+
+       pairs := strings.Split(args, ",")
+       for _, pair := range pairs {
+               kv := strings.Split(pair, "=")
+               if len(kv) != 2 {
+                       return fmt.Errorf("ARGS: invalid pair %q", pair)
+               }
+               keyString := kv[0]
+               valueString := kv[1]
+               keyField := containerValue.Elem().FieldByName(keyString)
+               if !keyField.IsValid() {
+                       return fmt.Errorf("ARGS: invalid key %q", keyString)
+               }
+               u := keyField.Addr().Interface().(encoding.TextUnmarshaler)
+               err := u.UnmarshalText([]byte(valueString))
+               if err != nil {
+                       return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err)
+               }
+       }
+       return nil
+}
index 0f3699d..8d20344 100644 (file)
@@ -78,6 +78,40 @@ func (a *IPAllocator) Get(id string) (*plugin.IPConfig, error) {
                gw = ip.NextIP(a.conf.Subnet.IP)
        }
 
+       var requestedIP net.IP
+       if a.conf.Args != nil {
+               requestedIP = a.conf.Args.IP
+       }
+
+       if requestedIP != nil {
+               if gw != nil && gw.Equal(a.conf.Args.IP) {
+                       return nil, fmt.Errorf("requested IP must differ gateway IP")
+               }
+
+               subnet := net.IPNet{
+                       IP:   a.conf.Subnet.IP,
+                       Mask: a.conf.Subnet.Mask,
+               }
+               err := validateRangeIP(requestedIP, &subnet)
+               if err != nil {
+                       return nil, err
+               }
+
+               reserved, err := a.store.Reserve(id, requestedIP)
+               if err != nil {
+                       return nil, err
+               }
+
+               if reserved {
+                       return &plugin.IPConfig{
+                               IP:      net.IPNet{requestedIP, a.conf.Subnet.Mask},
+                               Gateway: gw,
+                               Routes:  a.conf.Routes,
+                       }, nil
+               }
+               return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
+       }
+
        for cur := a.start; !cur.Equal(a.end); cur = ip.NextIP(cur) {
                // don't allocate gateway IP
                if gw != nil && cur.Equal(gw) {
@@ -96,7 +130,6 @@ func (a *IPAllocator) Get(id string) (*plugin.IPConfig, error) {
                        }, nil
                }
        }
-
        return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
 }
 
index 1dd017c..828b123 100644 (file)
@@ -32,6 +32,11 @@ type IPAMConfig struct {
        Subnet     ip.IPNet       `json:"subnet"`
        Gateway    net.IP         `json:"gateway"`
        Routes     []plugin.Route `json:"routes"`
+       Args       *IPAMArgs      `json:"-"`
+}
+
+type IPAMArgs struct {
+       IP net.IP `json:"ip",omitempty`
 }
 
 type Net struct {
@@ -40,12 +45,20 @@ type Net struct {
 }
 
 // NewIPAMConfig creates a NetworkConfig from the given network name.
-func LoadIPAMConfig(bytes []byte) (*IPAMConfig, error) {
+func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) {
        n := Net{}
        if err := json.Unmarshal(bytes, &n); err != nil {
                return nil, err
        }
 
+       if args != "" {
+               ipamArgs := IPAMArgs{}
+               err := plugin.LoadArgs(args, &ipamArgs)
+               if err != nil {
+                       return nil, err
+               }
+       }
+
        if n.IPAM == nil {
                return nil, fmt.Errorf("%q missing 'ipam' key")
        }
index 799b114..76b9e84 100644 (file)
@@ -28,7 +28,7 @@ func main() {
 }
 
 func cmdAdd(args *skel.CmdArgs) error {
-       ipamConf, err := LoadIPAMConfig(args.StdinData)
+       ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args)
        if err != nil {
                return err
        }
@@ -39,6 +39,13 @@ func cmdAdd(args *skel.CmdArgs) error {
        }
        defer store.Close()
 
+       ipamArgs := IPAMArgs{}
+       err = plugin.LoadArgs(args.Args, &ipamArgs)
+       if err != nil {
+               return err
+       }
+       ipamConf.Args = &ipamArgs
+
        allocator, err := NewIPAllocator(ipamConf, store)
        if err != nil {
                return err
@@ -66,7 +73,7 @@ func cmdAdd(args *skel.CmdArgs) error {
 }
 
 func cmdDel(args *skel.CmdArgs) error {
-       ipamConf, err := LoadIPAMConfig(args.StdinData)
+       ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args)
        if err != nil {
                return err
        }