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 }