Merge pull request #670 from SilverBut/ipam-dhcp-more-options
dhcp ipam: support customizing dhcp options from CNI args
This commit is contained in:
commit
cc32993e9e
@ -29,13 +29,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containernetworking/cni/pkg/skel"
|
"github.com/containernetworking/cni/pkg/skel"
|
||||||
"github.com/containernetworking/cni/pkg/types"
|
|
||||||
current "github.com/containernetworking/cni/pkg/types/100"
|
current "github.com/containernetworking/cni/pkg/types/100"
|
||||||
"github.com/coreos/go-systemd/v22/activation"
|
"github.com/coreos/go-systemd/v22/activation"
|
||||||
)
|
)
|
||||||
|
|
||||||
const listenFdsStart = 3
|
|
||||||
|
|
||||||
var errNoMoreTries = errors.New("no more tries")
|
var errNoMoreTries = errors.New("no more tries")
|
||||||
|
|
||||||
type DHCP struct {
|
type DHCP struct {
|
||||||
@ -55,21 +52,35 @@ func newDHCP(clientTimeout, clientResendMax time.Duration) *DHCP {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: current client ID is too long. At least the container ID should not be used directly.
|
||||||
|
// A seperate issue is necessary to ensure no breaking change is affecting other users.
|
||||||
func generateClientID(containerID string, netName string, ifName string) string {
|
func generateClientID(containerID string, netName string, ifName string) string {
|
||||||
return containerID + "/" + netName + "/" + ifName
|
clientID := containerID + "/" + netName + "/" + ifName
|
||||||
|
// defined in RFC 2132, length size can not be larger than 1 octet. So we truncate 254 to make everyone happy.
|
||||||
|
if len(clientID) > 254 {
|
||||||
|
clientID = clientID[0:254]
|
||||||
|
}
|
||||||
|
return clientID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate acquires an IP from a DHCP server for a specified container.
|
// Allocate acquires an IP from a DHCP server for a specified container.
|
||||||
// The acquired lease will be maintained until Release() is called.
|
// The acquired lease will be maintained until Release() is called.
|
||||||
func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
||||||
conf := types.NetConf{}
|
conf := NetConf{}
|
||||||
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||||
return fmt.Errorf("error parsing netconf: %v", err)
|
return fmt.Errorf("error parsing netconf: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optsRequesting, optsProviding, err := prepareOptions(args.Args, conf.IPAM.ProvideOptions, conf.IPAM.RequestOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
|
clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
|
||||||
hostNetns := d.hostNetnsPrefix + args.Netns
|
hostNetns := d.hostNetnsPrefix + args.Netns
|
||||||
l, err := AcquireLease(clientID, hostNetns, args.IfName, d.clientTimeout, d.clientResendMax, d.broadcast)
|
l, err := AcquireLease(clientID, hostNetns, args.IfName,
|
||||||
|
optsRequesting, optsProviding,
|
||||||
|
d.clientTimeout, d.clientResendMax, d.broadcast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -94,7 +105,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
|
|||||||
// Release stops maintenance of the lease acquired in Allocate()
|
// Release stops maintenance of the lease acquired in Allocate()
|
||||||
// and sends a release msg to the DHCP server.
|
// and sends a release msg to the DHCP server.
|
||||||
func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
|
func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error {
|
||||||
conf := types.NetConf{}
|
conf := NetConf{}
|
||||||
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
|
||||||
return fmt.Errorf("error parsing netconf: %v", err)
|
return fmt.Errorf("error parsing netconf: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@ -36,6 +37,11 @@ import (
|
|||||||
const resendDelay0 = 4 * time.Second
|
const resendDelay0 = 4 * time.Second
|
||||||
const resendDelayMax = 62 * time.Second
|
const resendDelayMax = 62 * time.Second
|
||||||
|
|
||||||
|
// To speed up the retry for first few failures, we retry without
|
||||||
|
// backoff for a few times
|
||||||
|
const resendFastDelay = 2 * time.Second
|
||||||
|
const resendFastMax = 4
|
||||||
|
|
||||||
const (
|
const (
|
||||||
leaseStateBound = iota
|
leaseStateBound = iota
|
||||||
leaseStateRenewing
|
leaseStateRenewing
|
||||||
@ -62,6 +68,74 @@ type DHCPLease struct {
|
|||||||
stopping uint32
|
stopping uint32
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
// list of requesting and providing options and if they are necessary / their value
|
||||||
|
optsRequesting map[dhcp4.OptionCode]bool
|
||||||
|
optsProviding map[dhcp4.OptionCode][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestOptionsDefault = map[dhcp4.OptionCode]bool{
|
||||||
|
dhcp4.OptionRouter: true,
|
||||||
|
dhcp4.OptionSubnetMask: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareOptions(cniArgs string, ProvideOptions []ProvideOption, RequestOptions []RequestOption) (
|
||||||
|
optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte, err error) {
|
||||||
|
|
||||||
|
// parse CNI args
|
||||||
|
cniArgsParsed := map[string]string{}
|
||||||
|
for _, argPair := range strings.Split(cniArgs, ";") {
|
||||||
|
args := strings.SplitN(argPair, "=", 2)
|
||||||
|
if len(args) > 1 {
|
||||||
|
cniArgsParsed[args[0]] = args[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse providing options map
|
||||||
|
var optParsed dhcp4.OptionCode
|
||||||
|
optsProviding = make(map[dhcp4.OptionCode][]byte)
|
||||||
|
for _, opt := range ProvideOptions {
|
||||||
|
optParsed, err = parseOptionName(string(opt.Option))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(opt.Value) > 0 {
|
||||||
|
if len(opt.Value) > 255 {
|
||||||
|
err = fmt.Errorf("value too long for option %q: %q", opt.Option, opt.Value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optsProviding[optParsed] = []byte(opt.Value)
|
||||||
|
}
|
||||||
|
if value, ok := cniArgsParsed[opt.ValueFromCNIArg]; ok {
|
||||||
|
if len(value) > 255 {
|
||||||
|
err = fmt.Errorf("value too long for option %q from CNI_ARGS %q: %q", opt.Option, opt.ValueFromCNIArg, opt.Value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optsProviding[optParsed] = []byte(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse necessary options map
|
||||||
|
optsRequesting = make(map[dhcp4.OptionCode]bool)
|
||||||
|
skipRequireDefault := false
|
||||||
|
for _, opt := range RequestOptions {
|
||||||
|
if opt.SkipDefault {
|
||||||
|
skipRequireDefault = true
|
||||||
|
}
|
||||||
|
optParsed, err = parseOptionName(string(opt.Option))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Can not parse option %q: %w", opt.Option, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optsRequesting[optParsed] = true
|
||||||
|
}
|
||||||
|
for k, v := range requestOptionsDefault {
|
||||||
|
// only set if not skipping default and this value does not exists
|
||||||
|
if _, ok := optsRequesting[k]; !ok && !skipRequireDefault {
|
||||||
|
optsRequesting[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcquireLease gets an DHCP lease and then maintains it in the background
|
// AcquireLease gets an DHCP lease and then maintains it in the background
|
||||||
@ -69,6 +143,7 @@ type DHCPLease struct {
|
|||||||
// calling DHCPLease.Stop()
|
// calling DHCPLease.Stop()
|
||||||
func AcquireLease(
|
func AcquireLease(
|
||||||
clientID, netns, ifName string,
|
clientID, netns, ifName string,
|
||||||
|
optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte,
|
||||||
timeout, resendMax time.Duration, broadcast bool,
|
timeout, resendMax time.Duration, broadcast bool,
|
||||||
) (*DHCPLease, error) {
|
) (*DHCPLease, error) {
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
@ -78,6 +153,8 @@ func AcquireLease(
|
|||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
resendMax: resendMax,
|
resendMax: resendMax,
|
||||||
broadcast: broadcast,
|
broadcast: broadcast,
|
||||||
|
optsRequesting: optsRequesting,
|
||||||
|
optsProviding: optsProviding,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("%v: acquiring lease", clientID)
|
log.Printf("%v: acquiring lease", clientID)
|
||||||
@ -139,7 +216,17 @@ func (l *DHCPLease) acquire() error {
|
|||||||
|
|
||||||
opts := make(dhcp4.Options)
|
opts := make(dhcp4.Options)
|
||||||
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
|
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID)
|
||||||
opts[dhcp4.OptionParameterRequestList] = []byte{byte(dhcp4.OptionRouter), byte(dhcp4.OptionSubnetMask)}
|
opts[dhcp4.OptionParameterRequestList] = []byte{}
|
||||||
|
for k := range l.optsRequesting {
|
||||||
|
opts[dhcp4.OptionParameterRequestList] = append(opts[dhcp4.OptionParameterRequestList], byte(k))
|
||||||
|
}
|
||||||
|
for k, v := range l.optsProviding {
|
||||||
|
opts[k] = v
|
||||||
|
}
|
||||||
|
// client identifier's first byte is "type"
|
||||||
|
newClientID := []byte{0}
|
||||||
|
newClientID = append(newClientID, opts[dhcp4.OptionClientIdentifier]...)
|
||||||
|
opts[dhcp4.OptionClientIdentifier] = newClientID
|
||||||
|
|
||||||
pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) {
|
pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) {
|
||||||
ok, ack, err := DhcpRequest(c, opts)
|
ok, ack, err := DhcpRequest(c, opts)
|
||||||
@ -345,7 +432,7 @@ func jitter(span time.Duration) time.Duration {
|
|||||||
func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
|
func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) {
|
||||||
var baseDelay time.Duration = resendDelay0
|
var baseDelay time.Duration = resendDelay0
|
||||||
var sleepTime time.Duration
|
var sleepTime time.Duration
|
||||||
|
var fastRetryLimit = resendFastMax
|
||||||
for {
|
for {
|
||||||
pkt, err := f()
|
pkt, err := f()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -354,13 +441,19 @@ func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dh
|
|||||||
|
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|
||||||
|
if fastRetryLimit == 0 {
|
||||||
sleepTime = baseDelay + jitter(time.Second)
|
sleepTime = baseDelay + jitter(time.Second)
|
||||||
|
} else {
|
||||||
|
sleepTime = resendFastDelay + jitter(time.Second)
|
||||||
|
fastRetryLimit--
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("retrying in %f seconds", sleepTime.Seconds())
|
log.Printf("retrying in %f seconds", sleepTime.Seconds())
|
||||||
|
|
||||||
time.Sleep(sleepTime)
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
if baseDelay < resendMax {
|
// only adjust delay time if we are in normal backoff stage
|
||||||
|
if baseDelay < resendMax && fastRetryLimit == 0 {
|
||||||
baseDelay *= 2
|
baseDelay *= 2
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -33,6 +33,43 @@ import (
|
|||||||
|
|
||||||
const defaultSocketPath = "/run/cni/dhcp.sock"
|
const defaultSocketPath = "/run/cni/dhcp.sock"
|
||||||
|
|
||||||
|
// The top-level network config - IPAM plugins are passed the full configuration
|
||||||
|
// of the calling plugin, not just the IPAM section.
|
||||||
|
type NetConf struct {
|
||||||
|
types.NetConf
|
||||||
|
IPAM *IPAMConfig `json:"ipam"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPAMConfig struct {
|
||||||
|
types.IPAM
|
||||||
|
DaemonSocketPath string `json:"daemonSocketPath"`
|
||||||
|
// When requesting IP from DHCP server, carry these options for management purpose.
|
||||||
|
// Some fields have default values, and can be override by setting a new option with the same name at here.
|
||||||
|
ProvideOptions []ProvideOption `json:"provide"`
|
||||||
|
// When requesting IP from DHCP server, claiming these options are necessary. Options are necessary unless `optional`
|
||||||
|
// is set to `false`.
|
||||||
|
// To override default requesting fields, set `skipDefault` to `false`.
|
||||||
|
// If an field is not optional, but the server failed to provide it, error will be raised.
|
||||||
|
RequestOptions []RequestOption `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DHCPOption represents a DHCP option. It can be a number, or a string defined in manual dhcp-options(5).
|
||||||
|
// Note that not all DHCP options are supported at all time. Error will be raised if unsupported options are used.
|
||||||
|
type DHCPOption string
|
||||||
|
|
||||||
|
type ProvideOption struct {
|
||||||
|
Option DHCPOption `json:"option"`
|
||||||
|
|
||||||
|
Value string `json:"value"`
|
||||||
|
ValueFromCNIArg string `json:"fromArg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOption struct {
|
||||||
|
SkipDefault bool `json:"skipDefault"`
|
||||||
|
|
||||||
|
Option DHCPOption `json:"option"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) > 1 && os.Args[1] == "daemon" {
|
if len(os.Args) > 1 && os.Args[1] == "daemon" {
|
||||||
var pidfilePath string
|
var pidfilePath string
|
||||||
@ -55,7 +92,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := runDaemon(pidfilePath, hostPrefix, socketPath, timeout, resendMax, broadcast); err != nil {
|
if err := runDaemon(pidfilePath, hostPrefix, socketPath, timeout, resendMax, broadcast); err != nil {
|
||||||
log.Printf(err.Error())
|
log.Print(err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -88,8 +125,6 @@ func cmdDel(args *skel.CmdArgs) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdCheck(args *skel.CmdArgs) error {
|
func cmdCheck(args *skel.CmdArgs) error {
|
||||||
// TODO: implement
|
|
||||||
//return fmt.Errorf("not implemented")
|
|
||||||
// Plugin must return result in same version as specified in netconf
|
// Plugin must return result in same version as specified in netconf
|
||||||
versionDecoder := &version.ConfigDecoder{}
|
versionDecoder := &version.ConfigDecoder{}
|
||||||
//confVersion, err := versionDecoder.Decode(args.StdinData)
|
//confVersion, err := versionDecoder.Decode(args.StdinData)
|
||||||
@ -106,16 +141,8 @@ func cmdCheck(args *skel.CmdArgs) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocketPathConf struct {
|
|
||||||
DaemonSocketPath string `json:"daemonSocketPath,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TempNetConf struct {
|
|
||||||
IPAM SocketPathConf `json:"ipam,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSocketPath(stdinData []byte) (string, error) {
|
func getSocketPath(stdinData []byte) (string, error) {
|
||||||
conf := TempNetConf{}
|
conf := NetConf{}
|
||||||
if err := json.Unmarshal(stdinData, &conf); err != nil {
|
if err := json.Unmarshal(stdinData, &conf); err != nil {
|
||||||
return "", fmt.Errorf("error parsing socket path conf: %v", err)
|
return "", fmt.Errorf("error parsing socket path conf: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,33 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containernetworking/cni/pkg/types"
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
"github.com/d2g/dhcp4"
|
"github.com/d2g/dhcp4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var optionNameToID = map[string]dhcp4.OptionCode{
|
||||||
|
"dhcp-client-identifier": dhcp4.OptionClientIdentifier,
|
||||||
|
"subnet-mask": dhcp4.OptionSubnetMask,
|
||||||
|
"routers": dhcp4.OptionRouter,
|
||||||
|
"host-name": dhcp4.OptionHostName,
|
||||||
|
"user-class": dhcp4.OptionUserClass,
|
||||||
|
"vendor-class-identifier": dhcp4.OptionVendorClassIdentifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptionName(option string) (dhcp4.OptionCode, error) {
|
||||||
|
if val, ok := optionNameToID[option]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
i, err := strconv.ParseUint(option, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("Can not parse option: %w", err)
|
||||||
|
}
|
||||||
|
return dhcp4.OptionCode(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseRouter(opts dhcp4.Options) net.IP {
|
func parseRouter(opts dhcp4.Options) net.IP {
|
||||||
if opts, ok := opts[dhcp4.OptionRouter]; ok {
|
if opts, ok := opts[dhcp4.OptionRouter]; ok {
|
||||||
if len(opts) == 4 {
|
if len(opts) == 4 {
|
||||||
|
@ -16,6 +16,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containernetworking/cni/pkg/types"
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
@ -73,3 +74,34 @@ func TestParseCIDRRoutes(t *testing.T) {
|
|||||||
|
|
||||||
validateRoutes(t, routes)
|
validateRoutes(t, routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseOptionName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
option string
|
||||||
|
want dhcp4.OptionCode
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"hostname", "host-name", dhcp4.OptionHostName, false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostname in number", "12", dhcp4.OptionHostName, false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"random string", "doNotparseMe", 0, true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := parseOptionName(tt.option)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseOptionName() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("parseOptionName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user