github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocrunner/networking_cni.go (about)

     1  // For now CNI is supported only on Linux.
     2  //
     3  //+build linux
     4  
     5  package allocrunner
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"math/rand"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	cni "github.com/containerd/go-cni"
    18  	cnilibrary "github.com/containernetworking/cni/libcni"
    19  	log "github.com/hashicorp/go-hclog"
    20  	"github.com/hashicorp/nomad/nomad/structs"
    21  	"github.com/hashicorp/nomad/plugins/drivers"
    22  )
    23  
    24  const (
    25  
    26  	// envCNIPath is the environment variable name to use to derive the CNI path
    27  	// when it is not explicitly set by the client
    28  	envCNIPath = "CNI_PATH"
    29  
    30  	// defaultCNIPath is the CNI path to use when it is not set by the client
    31  	// and is not set by environment variable
    32  	defaultCNIPath = "/opt/cni/bin"
    33  
    34  	// defaultCNIInterfacePrefix is the network interface to use if not set in
    35  	// client config
    36  	defaultCNIInterfacePrefix = "eth"
    37  )
    38  
    39  type cniNetworkConfigurator struct {
    40  	cni                     cni.CNI
    41  	cniConf                 []byte
    42  	ignorePortMappingHostIP bool
    43  
    44  	rand   *rand.Rand
    45  	logger log.Logger
    46  }
    47  
    48  func newCNINetworkConfigurator(logger log.Logger, cniPath, cniInterfacePrefix, cniConfDir, networkName string, ignorePortMappingHostIP bool) (*cniNetworkConfigurator, error) {
    49  	cniConf, err := loadCNIConf(cniConfDir, networkName)
    50  	if err != nil {
    51  		return nil, fmt.Errorf("failed to load CNI config: %v", err)
    52  	}
    53  
    54  	return newCNINetworkConfiguratorWithConf(logger, cniPath, cniInterfacePrefix, ignorePortMappingHostIP, cniConf)
    55  }
    56  
    57  func newCNINetworkConfiguratorWithConf(logger log.Logger, cniPath, cniInterfacePrefix string, ignorePortMappingHostIP bool, cniConf []byte) (*cniNetworkConfigurator, error) {
    58  	conf := &cniNetworkConfigurator{
    59  		cniConf:                 cniConf,
    60  		rand:                    rand.New(rand.NewSource(time.Now().Unix())),
    61  		logger:                  logger,
    62  		ignorePortMappingHostIP: ignorePortMappingHostIP,
    63  	}
    64  	if cniPath == "" {
    65  		if cniPath = os.Getenv(envCNIPath); cniPath == "" {
    66  			cniPath = defaultCNIPath
    67  		}
    68  	}
    69  
    70  	if cniInterfacePrefix == "" {
    71  		cniInterfacePrefix = defaultCNIInterfacePrefix
    72  	}
    73  
    74  	c, err := cni.New(cni.WithPluginDir(filepath.SplitList(cniPath)),
    75  		cni.WithInterfacePrefix(cniInterfacePrefix))
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  	conf.cni = c
    80  
    81  	return conf, nil
    82  }
    83  
    84  // Setup calls the CNI plugins with the add action
    85  func (c *cniNetworkConfigurator) Setup(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) (*structs.AllocNetworkStatus, error) {
    86  	if err := c.ensureCNIInitialized(); err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	// Depending on the version of bridge cni plugin used, a known race could occure
    91  	// where two alloc attempt to create the nomad bridge at the same time, resulting
    92  	// in one of them to fail. This rety attempts to overcome those erroneous failures.
    93  	const retry = 3
    94  	var firstError error
    95  	var res *cni.CNIResult
    96  	for attempt := 1; ; attempt++ {
    97  		var err error
    98  		if res, err = c.cni.Setup(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc, c.ignorePortMappingHostIP))); err != nil {
    99  			c.logger.Warn("failed to configure network", "err", err, "attempt", attempt)
   100  			switch attempt {
   101  			case 1:
   102  				firstError = err
   103  			case retry:
   104  				return nil, fmt.Errorf("failed to configure network: %v", firstError)
   105  			}
   106  
   107  			// Sleep for 1 second + jitter
   108  			time.Sleep(time.Second + (time.Duration(c.rand.Int63n(1000)) * time.Millisecond))
   109  			continue
   110  		}
   111  		break
   112  	}
   113  
   114  	netStatus := new(structs.AllocNetworkStatus)
   115  
   116  	if len(res.Interfaces) > 0 {
   117  		// find an interface with Sandbox set, or any one of them if no
   118  		// interface has it set
   119  		var iface *cni.Config
   120  		var name string
   121  		for name, iface = range res.Interfaces {
   122  			if iface != nil && iface.Sandbox != "" {
   123  				break
   124  			}
   125  		}
   126  		if iface == nil {
   127  			// this should never happen but this value is coming from external
   128  			// plugins so we should guard against it
   129  			return nil, fmt.Errorf("failed to configure network: no valid interface")
   130  		}
   131  
   132  		netStatus.InterfaceName = name
   133  		if len(iface.IPConfigs) > 0 {
   134  			netStatus.Address = iface.IPConfigs[0].IP.String()
   135  		}
   136  	}
   137  	if len(res.DNS) > 0 {
   138  		netStatus.DNS = &structs.DNSConfig{
   139  			Servers:  res.DNS[0].Nameservers,
   140  			Searches: res.DNS[0].Search,
   141  			Options:  res.DNS[0].Options,
   142  		}
   143  	}
   144  
   145  	return netStatus, nil
   146  
   147  }
   148  
   149  func loadCNIConf(confDir, name string) ([]byte, error) {
   150  	files, err := cnilibrary.ConfFiles(confDir, []string{".conf", ".conflist", ".json"})
   151  	switch {
   152  	case err != nil:
   153  		return nil, fmt.Errorf("failed to detect CNI config file: %v", err)
   154  	case len(files) == 0:
   155  		return nil, fmt.Errorf("no CNI network config found in %s", confDir)
   156  	}
   157  
   158  	// files contains the network config files associated with cni network.
   159  	// Use lexicographical way as a defined order for network config files.
   160  	sort.Strings(files)
   161  	for _, confFile := range files {
   162  		if strings.HasSuffix(confFile, ".conflist") {
   163  			confList, err := cnilibrary.ConfListFromFile(confFile)
   164  			if err != nil {
   165  				return nil, fmt.Errorf("failed to load CNI config list file %s: %v", confFile, err)
   166  			}
   167  			if confList.Name == name {
   168  				return confList.Bytes, nil
   169  			}
   170  		} else {
   171  			conf, err := cnilibrary.ConfFromFile(confFile)
   172  			if err != nil {
   173  				return nil, fmt.Errorf("failed to load CNI config file %s: %v", confFile, err)
   174  			}
   175  			if conf.Network.Name == name {
   176  				return conf.Bytes, nil
   177  			}
   178  		}
   179  	}
   180  
   181  	return nil, fmt.Errorf("CNI network config not found for name %q", name)
   182  }
   183  
   184  // Teardown calls the CNI plugins with the delete action
   185  func (c *cniNetworkConfigurator) Teardown(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
   186  	if err := c.ensureCNIInitialized(); err != nil {
   187  		return err
   188  	}
   189  
   190  	return c.cni.Remove(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc, c.ignorePortMappingHostIP)))
   191  }
   192  
   193  func (c *cniNetworkConfigurator) ensureCNIInitialized() error {
   194  	if err := c.cni.Status(); cni.IsCNINotInitialized(err) {
   195  		return c.cni.Load(cni.WithConfListBytes(c.cniConf))
   196  	} else {
   197  		return err
   198  	}
   199  }
   200  
   201  // getPortMapping builds a list of portMapping structs that are used as the
   202  // portmapping capability arguments for the portmap CNI plugin
   203  func getPortMapping(alloc *structs.Allocation, ignoreHostIP bool) []cni.PortMapping {
   204  	ports := []cni.PortMapping{}
   205  
   206  	if len(alloc.AllocatedResources.Shared.Ports) == 0 && len(alloc.AllocatedResources.Shared.Networks) > 0 {
   207  		for _, network := range alloc.AllocatedResources.Shared.Networks {
   208  			for _, port := range append(network.DynamicPorts, network.ReservedPorts...) {
   209  				if port.To < 1 {
   210  					port.To = port.Value
   211  				}
   212  				for _, proto := range []string{"tcp", "udp"} {
   213  					ports = append(ports, cni.PortMapping{
   214  						HostPort:      int32(port.Value),
   215  						ContainerPort: int32(port.To),
   216  						Protocol:      proto,
   217  					})
   218  				}
   219  			}
   220  		}
   221  	} else {
   222  		for _, port := range alloc.AllocatedResources.Shared.Ports {
   223  			if port.To < 1 {
   224  				port.To = port.Value
   225  			}
   226  			for _, proto := range []string{"tcp", "udp"} {
   227  				portMapping := cni.PortMapping{
   228  					HostPort:      int32(port.Value),
   229  					ContainerPort: int32(port.To),
   230  					Protocol:      proto,
   231  				}
   232  				if !ignoreHostIP {
   233  					portMapping.HostIP = port.HostIP
   234  				}
   235  				ports = append(ports, portMapping)
   236  			}
   237  		}
   238  	}
   239  	return ports
   240  }