github.com/hernad/nomad@v1.6.112/nomad/structs/network.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package structs
     5  
     6  import (
     7  	"fmt"
     8  	"math/rand"
     9  	"net"
    10  	"sync"
    11  
    12  	"golang.org/x/exp/maps"
    13  	"golang.org/x/exp/slices"
    14  )
    15  
    16  const (
    17  	// DefaultMinDynamicPort is the smallest dynamic port generated by
    18  	// default
    19  	DefaultMinDynamicPort = 20000
    20  
    21  	// DefaultMaxDynamicPort is the largest dynamic port generated by
    22  	// default
    23  	DefaultMaxDynamicPort = 32000
    24  
    25  	// maxRandPortAttempts is the maximum number of attempt
    26  	// to assign a random port
    27  	maxRandPortAttempts = 20
    28  
    29  	// MaxValidPort is the max valid port number
    30  	MaxValidPort = 65536
    31  )
    32  
    33  var (
    34  	// bitmapPool is used to pool the bitmaps used for port collision
    35  	// checking. They are fairly large (8K) so we can re-use them to
    36  	// avoid GC pressure. Care should be taken to call Clear() on any
    37  	// bitmap coming from the pool.
    38  	bitmapPool = new(sync.Pool)
    39  )
    40  
    41  // NetworkIndex is used to index the available network resources
    42  // and the used network resources on a machine given allocations
    43  //
    44  // Fields are exported so they may be JSON serialized for debugging.
    45  // Fields are *not* intended to be used directly.
    46  type NetworkIndex struct {
    47  	// TaskNetworks are the node networks available for
    48  	// task.resources.network asks.
    49  	TaskNetworks []*NetworkResource
    50  
    51  	// GroupNetworks are the node networks available for group.network
    52  	// asks.
    53  	GroupNetworks []*NodeNetworkResource
    54  
    55  	// HostNetworks indexes addresses by host network alias
    56  	HostNetworks map[string][]NodeNetworkAddress
    57  
    58  	// UsedPorts tracks which ports are used on a per-IP address basis. For
    59  	// example if a node has `network_interface=lo` and port 22 reserved,
    60  	// then on a dual stack loopback interface UsedPorts would contain:
    61  	// {
    62  	//  "127.0.0.1": Bitmap{22},
    63  	//  "::1":       Bitmap{22},
    64  	// }
    65  	UsedPorts map[string]Bitmap
    66  
    67  	// Deprecated bandwidth fields
    68  	AvailBandwidth map[string]int // Bandwidth by device
    69  	UsedBandwidth  map[string]int // Bandwidth by device
    70  
    71  	MinDynamicPort int // The smallest dynamic port generated
    72  	MaxDynamicPort int // The largest dynamic port generated
    73  }
    74  
    75  // NewNetworkIndex is used to construct a new network index
    76  func NewNetworkIndex() *NetworkIndex {
    77  	return &NetworkIndex{
    78  		HostNetworks:   make(map[string][]NodeNetworkAddress),
    79  		UsedPorts:      make(map[string]Bitmap),
    80  		AvailBandwidth: make(map[string]int),
    81  		UsedBandwidth:  make(map[string]int),
    82  		MinDynamicPort: DefaultMinDynamicPort,
    83  		MaxDynamicPort: DefaultMaxDynamicPort,
    84  	}
    85  }
    86  
    87  func (idx *NetworkIndex) getUsedPortsFor(ip string) Bitmap {
    88  	used := idx.UsedPorts[ip]
    89  	if used == nil {
    90  		// Try to get a bitmap from the pool, else create
    91  		raw := bitmapPool.Get()
    92  		if raw != nil {
    93  			used = raw.(Bitmap)
    94  			used.Clear()
    95  		} else {
    96  			used, _ = NewBitmap(MaxValidPort)
    97  		}
    98  		idx.UsedPorts[ip] = used
    99  	}
   100  	return used
   101  }
   102  
   103  func (idx *NetworkIndex) Copy() *NetworkIndex {
   104  	if idx == nil {
   105  		return nil
   106  	}
   107  
   108  	c := new(NetworkIndex)
   109  	*c = *idx
   110  
   111  	c.TaskNetworks = copyNetworkResources(idx.TaskNetworks)
   112  	c.GroupNetworks = copyNodeNetworks(idx.GroupNetworks)
   113  	c.HostNetworks = copyAvailAddresses(idx.HostNetworks)
   114  	if idx.AvailBandwidth != nil && len(idx.AvailBandwidth) == 0 {
   115  		c.AvailBandwidth = make(map[string]int)
   116  	} else {
   117  		c.AvailBandwidth = maps.Clone(idx.AvailBandwidth)
   118  	}
   119  	if len(idx.UsedPorts) > 0 {
   120  		c.UsedPorts = make(map[string]Bitmap, len(idx.UsedPorts))
   121  		for k, v := range idx.UsedPorts {
   122  			c.UsedPorts[k], _ = v.Copy()
   123  		}
   124  	}
   125  	if idx.UsedBandwidth != nil && len(idx.UsedBandwidth) == 0 {
   126  		c.UsedBandwidth = make(map[string]int)
   127  	} else {
   128  		c.UsedBandwidth = maps.Clone(idx.UsedBandwidth)
   129  	}
   130  
   131  	return c
   132  }
   133  
   134  func copyNetworkResources(resources []*NetworkResource) []*NetworkResource {
   135  	l := len(resources)
   136  	if l == 0 {
   137  		return nil
   138  	}
   139  
   140  	c := make([]*NetworkResource, l)
   141  	for i, resource := range resources {
   142  		c[i] = resource.Copy()
   143  	}
   144  	return c
   145  }
   146  
   147  func copyNodeNetworks(resources []*NodeNetworkResource) []*NodeNetworkResource {
   148  	l := len(resources)
   149  	if l == 0 {
   150  		return nil
   151  	}
   152  
   153  	c := make([]*NodeNetworkResource, l)
   154  	for i, resource := range resources {
   155  		c[i] = resource.Copy()
   156  	}
   157  	return c
   158  }
   159  
   160  func copyAvailAddresses(a map[string][]NodeNetworkAddress) map[string][]NodeNetworkAddress {
   161  	l := len(a)
   162  	if l == 0 {
   163  		return nil
   164  	}
   165  
   166  	c := make(map[string][]NodeNetworkAddress, l)
   167  	for k, v := range a {
   168  		if len(v) == 0 {
   169  			continue
   170  		}
   171  		c[k] = make([]NodeNetworkAddress, len(v))
   172  		copy(c[k], v)
   173  	}
   174  
   175  	return c
   176  }
   177  
   178  // Release is called when the network index is no longer needed
   179  // to attempt to re-use some of the memory it has allocated
   180  func (idx *NetworkIndex) Release() {
   181  	for _, b := range idx.UsedPorts {
   182  		bitmapPool.Put(b)
   183  	}
   184  }
   185  
   186  // Overcommitted checks if the network is overcommitted
   187  func (idx *NetworkIndex) Overcommitted() bool {
   188  	// TODO remove since bandwidth is deprecated
   189  	/*for device, used := range idx.UsedBandwidth {
   190  		avail := idx.AvailBandwidth[device]
   191  		if used > avail {
   192  			return true
   193  		}
   194  	}*/
   195  	return false
   196  }
   197  
   198  // SetNode is used to initialize a node's network index with available IPs,
   199  // reserved ports, and other details from a node's configuration and
   200  // fingerprinting.
   201  //
   202  // SetNode must be idempotent as preemption causes SetNode to be called
   203  // multiple times on the same NetworkIndex, only clearing UsedPorts between
   204  // calls.
   205  //
   206  // An error is returned if the Node cannot produce a consistent NetworkIndex
   207  // such as if reserved_ports are unparseable.
   208  //
   209  // Any errors returned by SetNode indicate a bug! The bug may lie in client
   210  // code not properly validating its configuration or it may lie in improper
   211  // Node object handling by servers. Users should not be able to cause SetNode
   212  // to error. Data that cause SetNode to error should be caught upstream such as
   213  // a client agent refusing to start with an invalid configuration.
   214  func (idx *NetworkIndex) SetNode(node *Node) error {
   215  
   216  	// COMPAT(0.11): Deprecated. taskNetworks are only used for
   217  	// task.resources.network asks which have been deprecated since before
   218  	// 0.11.
   219  	// Grab the network resources, handling both new and old Node layouts
   220  	// from clients.
   221  	var taskNetworks []*NetworkResource
   222  	if node.NodeResources != nil && len(node.NodeResources.Networks) != 0 {
   223  		taskNetworks = node.NodeResources.Networks
   224  	} else if node.Resources != nil {
   225  		taskNetworks = node.Resources.Networks
   226  	}
   227  
   228  	// Reserved ports get merged downward. For example given an agent
   229  	// config:
   230  	//
   231  	// client.reserved.reserved_ports = "22"
   232  	// client.host_network["eth0"] = {reserved_ports = "80,443"}
   233  	// client.host_network["eth1"] = {reserved_ports = "1-1000"}
   234  	//
   235  	// Addresses on taskNetworks reserve port 22
   236  	// Addresses on eth0 reserve 22,80,443 (note 22 is also reserved!)
   237  	// Addresses on eth1 reserve 1-1000
   238  	globalResPorts := []uint{}
   239  
   240  	if node.ReservedResources != nil && node.ReservedResources.Networks.ReservedHostPorts != "" {
   241  		resPorts, err := ParsePortRanges(node.ReservedResources.Networks.ReservedHostPorts)
   242  		if err != nil {
   243  			// This is a fatal error that should have been
   244  			// prevented by client validation.
   245  			return fmt.Errorf("error parsing reserved_ports: %w", err)
   246  		}
   247  
   248  		globalResPorts = make([]uint, len(resPorts))
   249  		for i, p := range resPorts {
   250  			globalResPorts[i] = uint(p)
   251  		}
   252  	} else if node.Reserved != nil {
   253  		// COMPAT(0.11): Remove after 0.11. Nodes stopped reporting
   254  		// reserved ports under Node.Reserved.Resources in #4750 / v0.9
   255  		for _, n := range node.Reserved.Networks {
   256  			used := idx.getUsedPortsFor(n.IP)
   257  			for _, ports := range [][]Port{n.ReservedPorts, n.DynamicPorts} {
   258  				for _, p := range ports {
   259  					if p.Value > MaxValidPort || p.Value < 0 {
   260  						// This is a fatal error that
   261  						// should have been prevented
   262  						// by validation upstream.
   263  						return fmt.Errorf("invalid port %d for reserved_ports", p.Value)
   264  					}
   265  
   266  					globalResPorts = append(globalResPorts, uint(p.Value))
   267  					used.Set(uint(p.Value))
   268  				}
   269  			}
   270  
   271  			// Reserve mbits
   272  			if n.Device != "" {
   273  				idx.UsedBandwidth[n.Device] += n.MBits
   274  			}
   275  		}
   276  	}
   277  
   278  	// Filter task networks down to those with a device. For example
   279  	// taskNetworks may contain a "bridge" interface which has no device
   280  	// set and cannot be used to fulfill asks.
   281  	for _, n := range taskNetworks {
   282  		if n.Device != "" {
   283  			idx.TaskNetworks = append(idx.TaskNetworks, n)
   284  			idx.AvailBandwidth[n.Device] = n.MBits
   285  
   286  			// Reserve ports
   287  			used := idx.getUsedPortsFor(n.IP)
   288  			for _, p := range globalResPorts {
   289  				used.Set(p)
   290  			}
   291  		}
   292  	}
   293  
   294  	// nodeNetworks are used for group.network asks.
   295  	var nodeNetworks []*NodeNetworkResource
   296  	if node.NodeResources != nil && len(node.NodeResources.NodeNetworks) != 0 {
   297  		nodeNetworks = node.NodeResources.NodeNetworks
   298  	}
   299  
   300  	for _, n := range nodeNetworks {
   301  		for _, a := range n.Addresses {
   302  			// Index host networks by their unique alias for asks
   303  			// with group.network.port.host_network set.
   304  			idx.HostNetworks[a.Alias] = append(idx.HostNetworks[a.Alias], a)
   305  
   306  			// Mark reserved ports as used without worrying about
   307  			// collisions. This effectively merges
   308  			// client.reserved.reserved_ports into each
   309  			// host_network.
   310  			used := idx.getUsedPortsFor(a.Address)
   311  			for _, p := range globalResPorts {
   312  				used.Set(p)
   313  			}
   314  
   315  			// If ReservedPorts is set on the NodeNetwork, use it
   316  			// and the global reserved ports.
   317  			if a.ReservedPorts != "" {
   318  				rp, err := ParsePortRanges(a.ReservedPorts)
   319  				if err != nil {
   320  					// This is a fatal error that should
   321  					// have been prevented by validation
   322  					// upstream.
   323  					return fmt.Errorf("error parsing reserved_ports for network %q: %w", a.Alias, err)
   324  				}
   325  				for _, p := range rp {
   326  					used.Set(uint(p))
   327  				}
   328  			}
   329  		}
   330  	}
   331  
   332  	// Set dynamic port range (applies to all addresses)
   333  	if node.NodeResources != nil && node.NodeResources.MinDynamicPort > 0 {
   334  		idx.MinDynamicPort = node.NodeResources.MinDynamicPort
   335  	}
   336  
   337  	if node.NodeResources != nil && node.NodeResources.MaxDynamicPort > 0 {
   338  		idx.MaxDynamicPort = node.NodeResources.MaxDynamicPort
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  // AddAllocs is used to add the used network resources. Returns
   345  // true if there is a collision
   346  //
   347  // AddAllocs may be called multiple times for the same NetworkIndex with
   348  // UsedPorts cleared between calls (by Release). Therefore AddAllocs must be
   349  // determistic and must not manipulate state outside of UsedPorts as that state
   350  // would persist between Release calls.
   351  func (idx *NetworkIndex) AddAllocs(allocs []*Allocation) (collide bool, reason string) {
   352  	for _, alloc := range allocs {
   353  		// Do not consider the resource impact of terminal allocations
   354  		if alloc.ClientTerminalStatus() {
   355  			continue
   356  		}
   357  
   358  		if alloc.AllocatedResources != nil {
   359  			// Only look at AllocatedPorts if populated, otherwise use pre 0.12 logic
   360  			// COMPAT(1.0): Remove when network resources struct is removed.
   361  			if len(alloc.AllocatedResources.Shared.Ports) > 0 {
   362  				if c, r := idx.AddReservedPorts(alloc.AllocatedResources.Shared.Ports); c {
   363  					collide = true
   364  					reason = fmt.Sprintf("collision when reserving port for alloc %s: %v", alloc.ID, r)
   365  				}
   366  			} else {
   367  				// Add network resources that are at the task group level
   368  				if len(alloc.AllocatedResources.Shared.Networks) > 0 {
   369  					for _, network := range alloc.AllocatedResources.Shared.Networks {
   370  						if c, r := idx.AddReserved(network); c {
   371  							collide = true
   372  							reason = fmt.Sprintf("collision when reserving port for network %s in alloc %s: %v", network.IP, alloc.ID, r)
   373  						}
   374  					}
   375  				}
   376  
   377  				for task, resources := range alloc.AllocatedResources.Tasks {
   378  					if len(resources.Networks) == 0 {
   379  						continue
   380  					}
   381  					n := resources.Networks[0]
   382  					if c, r := idx.AddReserved(n); c {
   383  						collide = true
   384  						reason = fmt.Sprintf("collision when reserving port for network %s in task %s of alloc %s: %v", n.IP, task, alloc.ID, r)
   385  					}
   386  				}
   387  			}
   388  		} else {
   389  			// COMPAT(0.11): Remove in 0.11
   390  			for task, resources := range alloc.TaskResources {
   391  				if len(resources.Networks) == 0 {
   392  					continue
   393  				}
   394  				n := resources.Networks[0]
   395  				if c, r := idx.AddReserved(n); c {
   396  					collide = true
   397  					reason = fmt.Sprintf("(deprecated) collision when reserving port for network %s in task %s of alloc %s: %v", n.IP, task, alloc.ID, r)
   398  				}
   399  			}
   400  		}
   401  	}
   402  	return
   403  }
   404  
   405  // AddReserved is used to add a reserved network usage, returns true
   406  // if there is a port collision
   407  func (idx *NetworkIndex) AddReserved(n *NetworkResource) (collide bool, reasons []string) {
   408  	// Add the port usage
   409  	used := idx.getUsedPortsFor(n.IP)
   410  
   411  	for _, ports := range [][]Port{n.ReservedPorts, n.DynamicPorts} {
   412  		for _, port := range ports {
   413  			// Guard against invalid port
   414  			if port.Value < 0 || port.Value >= MaxValidPort {
   415  				return true, []string{fmt.Sprintf("invalid port %d", port.Value)}
   416  			}
   417  			if used.Check(uint(port.Value)) {
   418  				collide = true
   419  				reason := fmt.Sprintf("port %d already in use", port.Value)
   420  				reasons = append(reasons, reason)
   421  			} else {
   422  				used.Set(uint(port.Value))
   423  			}
   424  		}
   425  	}
   426  
   427  	// Add the bandwidth
   428  	idx.UsedBandwidth[n.Device] += n.MBits
   429  	return
   430  }
   431  
   432  func (idx *NetworkIndex) AddReservedPorts(ports AllocatedPorts) (collide bool, reasons []string) {
   433  	for _, port := range ports {
   434  		used := idx.getUsedPortsFor(port.HostIP)
   435  		if port.Value < 0 || port.Value >= MaxValidPort {
   436  			return true, []string{fmt.Sprintf("invalid port %d", port.Value)}
   437  		}
   438  		if used.Check(uint(port.Value)) {
   439  			collide = true
   440  			reason := fmt.Sprintf("port %d already in use", port.Value)
   441  			reasons = append(reasons, reason)
   442  		} else {
   443  			used.Set(uint(port.Value))
   444  		}
   445  	}
   446  
   447  	return
   448  }
   449  
   450  // AddReservedPortsForIP checks whether any reserved ports collide with those
   451  // in use for the IP address.
   452  func (idx *NetworkIndex) AddReservedPortsForIP(ports []uint64, ip string) (collide bool, reasons []string) {
   453  	used := idx.getUsedPortsFor(ip)
   454  	for _, port := range ports {
   455  		// Guard against invalid port
   456  		if port >= MaxValidPort {
   457  			return true, []string{fmt.Sprintf("invalid port %d", port)}
   458  		}
   459  		if used.Check(uint(port)) {
   460  			collide = true
   461  			reason := fmt.Sprintf("port %d already in use", port)
   462  			reasons = append(reasons, reason)
   463  		} else {
   464  			used.Set(uint(port))
   465  		}
   466  	}
   467  
   468  	return
   469  }
   470  
   471  // yieldIP is used to iteratively invoke the callback with
   472  // an available IP
   473  func (idx *NetworkIndex) yieldIP(cb func(net *NetworkResource, offerIP net.IP) bool) {
   474  	for _, n := range idx.TaskNetworks {
   475  		ip, ipnet, err := net.ParseCIDR(n.CIDR)
   476  		if err != nil {
   477  			continue
   478  		}
   479  		for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) {
   480  			if cb(n, ip) {
   481  				return
   482  			}
   483  		}
   484  	}
   485  }
   486  
   487  func incIP(ip net.IP) {
   488  	// Iterate over IP octects from right to left
   489  	for j := len(ip) - 1; j >= 0; j-- {
   490  
   491  		// Increment octect
   492  		ip[j]++
   493  
   494  		// If this octect did not wrap around to 0, it's the next IP to
   495  		// try. If it did wrap (p[j]==0), then the next octect is
   496  		// incremented.
   497  		if ip[j] > 0 {
   498  			break
   499  		}
   500  	}
   501  }
   502  
   503  // AssignPorts based on an ask from the scheduler processing a group.network
   504  // block. Supports multi-interfaces through node configured host_networks.
   505  //
   506  // AssignTaskNetwork supports the deprecated task.resources.network block.
   507  func (idx *NetworkIndex) AssignPorts(ask *NetworkResource) (AllocatedPorts, error) {
   508  	var offer AllocatedPorts
   509  	var portsInOffer []int
   510  
   511  	// index of host network name to slice of reserved ports, used during dynamic port assignment
   512  	reservedIdx := map[string][]Port{}
   513  
   514  	for _, port := range ask.ReservedPorts {
   515  		reservedIdx[port.HostNetwork] = append(reservedIdx[port.HostNetwork], port)
   516  
   517  		// allocPort is set in the inner for loop if a port mapping can be created
   518  		// if allocPort is still nil after the loop, the port wasn't available for reservation
   519  		var allocPort *AllocatedPortMapping
   520  		var addrErr error
   521  		for _, addr := range idx.HostNetworks[port.HostNetwork] {
   522  			used := idx.getUsedPortsFor(addr.Address)
   523  			// Guard against invalid port
   524  			if port.Value < 0 || port.Value >= MaxValidPort {
   525  				return nil, fmt.Errorf("invalid port %d (out of range)", port.Value)
   526  			}
   527  
   528  			// Check if in use
   529  			if used != nil && used.Check(uint(port.Value)) {
   530  				return nil, fmt.Errorf("reserved port collision %s=%d", port.Label, port.Value)
   531  			}
   532  
   533  			allocPort = &AllocatedPortMapping{
   534  				Label:  port.Label,
   535  				Value:  port.Value,
   536  				To:     port.To,
   537  				HostIP: addr.Address,
   538  			}
   539  			break
   540  		}
   541  
   542  		if allocPort == nil {
   543  			if addrErr != nil {
   544  				return nil, addrErr
   545  			}
   546  
   547  			return nil, fmt.Errorf("no addresses available for %s network", port.HostNetwork)
   548  		}
   549  
   550  		offer = append(offer, *allocPort)
   551  		portsInOffer = append(portsInOffer, allocPort.Value)
   552  	}
   553  
   554  	for _, port := range ask.DynamicPorts {
   555  		var allocPort *AllocatedPortMapping
   556  		var addrErr error
   557  		for _, addr := range idx.HostNetworks[port.HostNetwork] {
   558  			used := idx.getUsedPortsFor(addr.Address)
   559  			// Try to stochastically pick the dynamic ports as it is faster and
   560  			// lower memory usage.
   561  			var dynPorts []int
   562  			// TODO: its more efficient to find multiple dynamic ports at once
   563  			dynPorts, addrErr = getDynamicPortsStochastic(
   564  				used, portsInOffer, idx.MinDynamicPort, idx.MaxDynamicPort,
   565  				reservedIdx[port.HostNetwork], 1)
   566  			if addrErr != nil {
   567  				// Fall back to the precise method if the random sampling failed.
   568  				dynPorts, addrErr = getDynamicPortsPrecise(used, portsInOffer,
   569  					idx.MinDynamicPort, idx.MaxDynamicPort,
   570  					reservedIdx[port.HostNetwork], 1)
   571  				if addrErr != nil {
   572  					continue
   573  				}
   574  			}
   575  
   576  			allocPort = &AllocatedPortMapping{
   577  				Label:  port.Label,
   578  				Value:  dynPorts[0],
   579  				To:     port.To,
   580  				HostIP: addr.Address,
   581  			}
   582  			if allocPort.To == -1 {
   583  				allocPort.To = allocPort.Value
   584  			}
   585  			break
   586  		}
   587  
   588  		if allocPort == nil {
   589  			if addrErr != nil {
   590  				return nil, addrErr
   591  			}
   592  
   593  			return nil, fmt.Errorf("no addresses available for %s network", port.HostNetwork)
   594  		}
   595  		offer = append(offer, *allocPort)
   596  		portsInOffer = append(portsInOffer, allocPort.Value)
   597  	}
   598  
   599  	return offer, nil
   600  }
   601  
   602  // AssignTaskNetwork is used to offer network resources given a
   603  // task.resources.network ask.  If the ask cannot be satisfied, returns nil
   604  //
   605  // AssignTaskNetwork and task.resources.network are deprecated in favor of
   606  // AssignPorts and group.network. AssignTaskNetwork does not support multiple
   607  // interfaces and only uses the node's default interface. AssignPorts is the
   608  // method that is used for group.network asks.
   609  func (idx *NetworkIndex) AssignTaskNetwork(ask *NetworkResource) (out *NetworkResource, err error) {
   610  	err = fmt.Errorf("no networks available")
   611  	idx.yieldIP(func(n *NetworkResource, offerIP net.IP) (stop bool) {
   612  		// Convert the IP to a string
   613  		offerIPStr := offerIP.String()
   614  
   615  		// Check if we would exceed the bandwidth cap
   616  		availBandwidth := idx.AvailBandwidth[n.Device]
   617  		usedBandwidth := idx.UsedBandwidth[n.Device]
   618  		if usedBandwidth+ask.MBits > availBandwidth {
   619  			err = fmt.Errorf("bandwidth exceeded")
   620  			return
   621  		}
   622  
   623  		used := idx.UsedPorts[offerIPStr]
   624  
   625  		// Check if any of the reserved ports are in use
   626  		for _, port := range ask.ReservedPorts {
   627  			// Guard against invalid port
   628  			if port.Value < 0 || port.Value >= MaxValidPort {
   629  				err = fmt.Errorf("invalid port %d (out of range)", port.Value)
   630  				return
   631  			}
   632  
   633  			// Check if in use
   634  			if used != nil && used.Check(uint(port.Value)) {
   635  				err = fmt.Errorf("reserved port collision %s=%d", port.Label, port.Value)
   636  				return
   637  			}
   638  		}
   639  
   640  		// Create the offer
   641  		offer := &NetworkResource{
   642  			Mode:          ask.Mode,
   643  			Device:        n.Device,
   644  			IP:            offerIPStr,
   645  			MBits:         ask.MBits,
   646  			DNS:           ask.DNS,
   647  			ReservedPorts: ask.ReservedPorts,
   648  			DynamicPorts:  ask.DynamicPorts,
   649  		}
   650  
   651  		// Try to stochastically pick the dynamic ports as it is faster and
   652  		// lower memory usage.
   653  		var dynPorts []int
   654  		var dynErr error
   655  		dynPorts, dynErr = getDynamicPortsStochastic(used, nil,
   656  			idx.MinDynamicPort, idx.MaxDynamicPort, ask.ReservedPorts, len(ask.DynamicPorts))
   657  		if dynErr == nil {
   658  			goto BUILD_OFFER
   659  		}
   660  
   661  		// Fall back to the precise method if the random sampling failed.
   662  		dynPorts, dynErr = getDynamicPortsPrecise(used, nil,
   663  			idx.MinDynamicPort, idx.MaxDynamicPort, ask.ReservedPorts, len(ask.DynamicPorts))
   664  		if dynErr != nil {
   665  			err = dynErr
   666  			return
   667  		}
   668  
   669  	BUILD_OFFER:
   670  		for i, port := range dynPorts {
   671  			offer.DynamicPorts[i].Value = port
   672  
   673  			// This syntax allows you to set the mapped to port to the same port
   674  			// allocated by the scheduler on the host.
   675  			if offer.DynamicPorts[i].To == -1 {
   676  				offer.DynamicPorts[i].To = port
   677  			}
   678  		}
   679  
   680  		// Stop, we have an offer!
   681  		out = offer
   682  		err = nil
   683  		return true
   684  	})
   685  	return
   686  }
   687  
   688  // getDynamicPortsPrecise takes the nodes used port bitmap which may be nil if
   689  // no ports have been allocated yet, any ports already offered in the caller,
   690  // and the network ask. It returns a set of unused ports to fulfil the ask's
   691  // DynamicPorts or an error if it failed. An error means the ask can not be
   692  // satisfied as the method does a precise search.
   693  func getDynamicPortsPrecise(nodeUsed Bitmap, portsInOffer []int, minDynamicPort, maxDynamicPort int, reserved []Port, numDyn int) ([]int, error) {
   694  	// Create a copy of the used ports and apply the new reserves
   695  	var usedSet Bitmap
   696  	var err error
   697  	if nodeUsed != nil {
   698  		usedSet, err = nodeUsed.Copy()
   699  		if err != nil {
   700  			return nil, err
   701  		}
   702  	} else {
   703  		usedSet, err = NewBitmap(MaxValidPort)
   704  		if err != nil {
   705  			return nil, err
   706  		}
   707  	}
   708  
   709  	for _, port := range reserved {
   710  		usedSet.Set(uint(port.Value))
   711  	}
   712  
   713  	// Get the indexes of the unset ports, less those which have already been
   714  	// picked as part of this offer
   715  	availablePorts := usedSet.IndexesInRangeFiltered(
   716  		false, uint(minDynamicPort), uint(maxDynamicPort), portsInOffer)
   717  
   718  	// Randomize the amount we need
   719  	if len(availablePorts) < numDyn {
   720  		return nil, fmt.Errorf("dynamic port selection failed")
   721  	}
   722  
   723  	numAvailable := len(availablePorts)
   724  	for i := 0; i < numDyn; i++ {
   725  		j := rand.Intn(numAvailable)
   726  		availablePorts[i], availablePorts[j] = availablePorts[j], availablePorts[i]
   727  	}
   728  
   729  	return availablePorts[:numDyn], nil
   730  }
   731  
   732  // getDynamicPortsStochastic takes the nodes used port bitmap which may be nil
   733  // if no ports have been allocated yet, any ports already offered in the caller,
   734  // and the network ask. It returns a set of unused ports to fulfil the ask's
   735  // DynamicPorts or an error if it failed. An error does not mean the ask can not
   736  // be satisfied as the method has a fixed amount of random probes and if these
   737  // fail, the search is aborted.
   738  func getDynamicPortsStochastic(nodeUsed Bitmap, portsInOffer []int, minDynamicPort, maxDynamicPort int, reservedPorts []Port, count int) ([]int, error) {
   739  	var reserved, dynamic []int
   740  	for _, port := range reservedPorts {
   741  		reserved = append(reserved, port.Value)
   742  	}
   743  
   744  	for i := 0; i < count; i++ {
   745  		attempts := 0
   746  	PICK:
   747  		attempts++
   748  		if attempts > maxRandPortAttempts {
   749  			return nil, fmt.Errorf("stochastic dynamic port selection failed")
   750  		}
   751  
   752  		randPort := minDynamicPort
   753  		if maxDynamicPort-minDynamicPort > 0 {
   754  			randPort = randPort + rand.Intn(maxDynamicPort-minDynamicPort)
   755  		}
   756  
   757  		if nodeUsed != nil && nodeUsed.Check(uint(randPort)) {
   758  			goto PICK
   759  		}
   760  
   761  		for _, ports := range [][]int{reserved, dynamic} {
   762  			if isPortReserved(ports, randPort) {
   763  				goto PICK
   764  			}
   765  		}
   766  		// the pick conflicted with a previous pick that hasn't been saved to
   767  		// the index yet
   768  		if slices.Contains(portsInOffer, randPort) {
   769  			goto PICK
   770  		}
   771  
   772  		dynamic = append(dynamic, randPort)
   773  	}
   774  
   775  	return dynamic, nil
   776  }
   777  
   778  // IntContains scans an integer slice for a value
   779  func isPortReserved(haystack []int, needle int) bool {
   780  	for _, item := range haystack {
   781  		if item == needle {
   782  			return true
   783  		}
   784  	}
   785  	return false
   786  }
   787  
   788  // AllocatedPortsToNetworkResouce is a COMPAT(1.0) remove when NetworkResource
   789  // is no longer used for materialized client view of ports.
   790  func AllocatedPortsToNetworkResouce(ask *NetworkResource, ports AllocatedPorts, node *NodeResources) *NetworkResource {
   791  	out := ask.Copy()
   792  
   793  	for i, port := range ask.DynamicPorts {
   794  		if p, ok := ports.Get(port.Label); ok {
   795  			out.DynamicPorts[i].Value = p.Value
   796  			out.DynamicPorts[i].To = p.To
   797  		}
   798  	}
   799  	if len(node.NodeNetworks) > 0 {
   800  		for _, nw := range node.NodeNetworks {
   801  			if nw.Mode == "host" {
   802  				out.IP = nw.Addresses[0].Address
   803  				break
   804  			}
   805  		}
   806  	} else {
   807  		for _, nw := range node.Networks {
   808  			if nw.Mode == "host" {
   809  				out.IP = nw.IP
   810  			}
   811  		}
   812  	}
   813  	return out
   814  }
   815  
   816  type ClientHostNetworkConfig struct {
   817  	Name          string `hcl:",key"`
   818  	CIDR          string `hcl:"cidr"`
   819  	Interface     string `hcl:"interface"`
   820  	ReservedPorts string `hcl:"reserved_ports"`
   821  }
   822  
   823  func (p *ClientHostNetworkConfig) Copy() *ClientHostNetworkConfig {
   824  	if p == nil {
   825  		return nil
   826  	}
   827  
   828  	c := new(ClientHostNetworkConfig)
   829  	*c = *p
   830  	return c
   831  }