diff options
author | HÃ¥kon Hallingstad <hakon@verizonmedia.com> | 2020-11-21 18:23:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-21 18:23:07 +0100 |
commit | 3112575fc713fa41232537a47d660bdf0a82e85e (patch) | |
tree | 91da5486a1c27052669d747a21206fe9081862f1 /node-repository | |
parent | dba9416bbd6a9039e46006364ab2356bdd0d1802 (diff) | |
parent | b1211ae56e3d7414f38f130ba104d95da83ac261 (diff) |
Merge pull request #15415 from vespa-engine/hakonhall/allow-allocating-to-a-provisioned-tenant-host
Allow allocating to a provisioned tenant host
Diffstat (limited to 'node-repository')
9 files changed, 210 insertions, 110 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index d0ee6229428..00327dc0002 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -94,7 +94,7 @@ public final class Node implements Nodelike { requireNonEmpty(ipConfig.primary(), "Active node " + hostname + " must have at least one valid IP address"); if (parentHostname.isPresent()) { - if (!ipConfig.pool().isEmpty()) throw new IllegalArgumentException("A child node cannot have an IP address pool"); + if (!ipConfig.pool().getIpSet().isEmpty()) throw new IllegalArgumentException("A child node cannot have an IP address pool"); if (modelName.isPresent()) throw new IllegalArgumentException("A child node cannot have model name set"); if (switchHostname.isPresent()) throw new IllegalArgumentException("A child node cannot have switch hostname set"); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index 05bdfd25b76..03ff89d36dc 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -460,7 +460,7 @@ public class NodeRepository extends AbstractComponent { .map(node -> { if (node.state() != State.provisioned && node.state() != State.dirty) illegal("Can not set " + node + " ready. It is not provisioned or dirty."); - if (node.type() == NodeType.host && node.ipConfig().pool().isEmpty()) + if (node.type() == NodeType.host && node.ipConfig().pool().getIpSet().isEmpty()) illegal("Can not set host " + node + " ready. Its IP address pool is empty."); return node.withWantToRetire(false, false, Agent.system, clock.instant()); }) diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java index 41d6c1e5425..bac31c40418 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.yahoo.config.provision.NodeType.confighost; import static com.yahoo.config.provision.NodeType.controllerhost; @@ -254,18 +255,25 @@ public class IP { * @return an allocation from the pool, if any can be made */ public Optional<Allocation> findAllocation(LockedNodeList nodes, NameResolver resolver) { + if (ipAddresses.asSet().isEmpty()) { + // IP addresses have not yet been resolved and should be done later. + return findUnusedAddressStream(nodes) + .map(Allocation::ofAddress) + .findFirst(); + } + if (ipAddresses.protocol == IpAddresses.Protocol.ipv4) { - return findUnused(nodes).stream() + return findUnusedIpAddresses(nodes).stream() .findFirst() .map(addr -> Allocation.ofIpv4(addr, resolver)); } - var unusedAddresses = findUnused(nodes); + var unusedAddresses = findUnusedIpAddresses(nodes); var allocation = unusedAddresses.stream() .filter(IP::isV6) .findFirst() .map(addr -> Allocation.ofIpv6(addr, resolver)); - allocation.flatMap(Allocation::secondary).ifPresent(ipv4Address -> { + allocation.flatMap(Allocation::ipv4Address).ifPresent(ipv4Address -> { if (!unusedAddresses.contains(ipv4Address)) { throw new IllegalArgumentException("Allocation resolved " + ipv4Address + " from hostname " + allocation.get().hostname + @@ -276,17 +284,43 @@ public class IP { } /** - * Finds all unused addresses in this pool + * Finds all unused IP addresses in this pool * * @param nodes a list of all nodes in the repository */ - public Set<String> findUnused(NodeList nodes) { + public Set<String> findUnusedIpAddresses(NodeList nodes) { var unusedAddresses = new LinkedHashSet<>(getIpSet()); nodes.matching(node -> node.ipConfig().primary().stream().anyMatch(ip -> getIpSet().contains(ip))) .forEach(node -> unusedAddresses.removeAll(node.ipConfig().primary())); return Collections.unmodifiableSet(unusedAddresses); } + /** + * Returns the number of unused IP addresses in the pool, assuming any and all unaccounted for hostnames + * in the pool are resolved to exactly 1 IP address (or 2 with {@link IpAddresses.Protocol#dualStack}). + */ + public int eventuallyUnusedAddressCount(NodeList nodes) { + // The address pool is filled immediately upon provisioning in dynamically provisioned zones, + // and within short time the IP address pool is filled. For all other cases, the IP address + // pool is already filled. + // + // The count in this method relies on the size of the IP address pool if that's non-empty, + // otherwise fall back to the address/hostname pool. + + + Set<String> currentIpAddresses = this.ipAddresses.asSet(); + if (!currentIpAddresses.isEmpty()) { + return findUnusedIpAddresses(nodes).size(); + } + + return (int) findUnusedAddressStream(nodes).count(); + } + + private Stream<Address> findUnusedAddressStream(NodeList nodes) { + Set<String> hostnames = nodes.stream().map(Node::hostname).collect(Collectors.toSet()); + return addresses.stream().filter(address -> !hostnames.contains(address.hostname())); + } + public IpAddresses.Protocol getProtocol() { return ipAddresses.protocol; } @@ -299,10 +333,6 @@ public class IP { return addresses; } - public boolean isEmpty() { - return getIpSet().isEmpty(); - } - public Pool withIpAddresses(Set<String> ipAddresses) { return Pool.of(ipAddresses, addresses); } @@ -326,22 +356,17 @@ public class IP { } - /** An IP address allocation from a pool */ + /** An address allocation from a pool */ public static class Allocation { private final String hostname; - private final String primary; - private final Optional<String> secondary; - - private Allocation(String hostname, String primary, Optional<String> secondary) { - Objects.requireNonNull(primary, "primary must be non-null"); - Objects.requireNonNull(secondary, "ipv4Address must be non-null"); - if (secondary.isPresent() && !isV4(secondary.get())) { // Secondary must be IPv4, if present - throw new IllegalArgumentException("Invalid IPv4 address '" + secondary + "'"); - } + private final Optional<String> ipv4Address; + private final Optional<String> ipv6Address; + + private Allocation(String hostname, Optional<String> ipv4Address, Optional<String> ipv6Address) { this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); - this.primary = primary; - this.secondary = secondary; + this.ipv4Address = Objects.requireNonNull(ipv4Address, "ipv4Address must be non-null"); + this.ipv6Address = Objects.requireNonNull(ipv6Address, "ipv6Address must be non-null"); } /** @@ -350,13 +375,17 @@ public class IP { * A successful allocation is guaranteed to have an IPv6 address, but may also have an IPv4 address if the * hostname of the IPv6 address has an A record. * - * @param ipAddress Unassigned IPv6 address + * @param ipv6Address Unassigned IPv6 address * @param resolver DNS name resolver to use * @throws IllegalArgumentException if DNS is misconfigured * @return An allocation containing 1 IPv6 address and 1 IPv4 address (if hostname is dual-stack) */ - private static Allocation ofIpv6(String ipAddress, NameResolver resolver) { - String hostname6 = resolver.resolveHostname(ipAddress).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + ipAddress)); + private static Allocation ofIpv6(String ipv6Address, NameResolver resolver) { + if (!isV6(ipv6Address)) { + throw new IllegalArgumentException("Invalid IPv6 address '" + ipv6Address + "'"); + } + + String hostname6 = resolver.resolveHostname(ipv6Address).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + ipv6Address)); List<String> ipv4Addresses = resolver.resolveAll(hostname6).stream() .filter(IP::isV4) .collect(Collectors.toList()); @@ -369,10 +398,10 @@ public class IP { if (!hostname6.equals(hostname4)) { throw new IllegalArgumentException(String.format("Hostnames resolved from each IP address do not " + "point to the same hostname [%s -> %s, %s -> %s]", - ipAddress, hostname6, addr, hostname4)); + ipv6Address, hostname6, addr, hostname4)); } }); - return new Allocation(hostname6, ipAddress, ipv4Address); + return new Allocation(hostname6, ipv4Address, Optional.of(ipv6Address)); } /** @@ -391,7 +420,11 @@ public class IP { throw new IllegalArgumentException("Hostname " + hostname4 + " did not resolve to exactly 1 address. " + "Resolved: " + addresses); } - return new Allocation(hostname4, addresses.get(0), Optional.empty()); + return new Allocation(hostname4, Optional.of(addresses.get(0)), Optional.empty()); + } + + private static Allocation ofAddress(Address address) { + return new Allocation(address.hostname(), Optional.empty(), Optional.empty()); } /** Hostname pointing to the IP addresses in this */ @@ -399,27 +432,28 @@ public class IP { return hostname; } - /** Primary address of this allocation */ - public String primary() { - return primary; + /** IPv4 address of this allocation */ + public Optional<String> ipv4Address() { + return ipv4Address; } - /** Secondary address of this allocation */ - public Optional<String> secondary() { - return secondary; + /** IPv6 address of this allocation */ + public Optional<String> ipv6Address() { + return ipv6Address; } /** All IP addresses in this */ public Set<String> addresses() { ImmutableSet.Builder<String> builder = ImmutableSet.builder(); - secondary.ifPresent(builder::add); - builder.add(primary); + ipv4Address.ifPresent(builder::add); + ipv6Address.ifPresent(builder::add); return builder.build(); } @Override public String toString() { - return String.format("IP allocation [primary=%s, secondary=%s]", primary, secondary.orElse("<none>")); + return String.format("Address allocation [hostname=%s, IPv4=%s, IPv6=%s]", + hostname, ipv4Address.orElse("<none>"), ipv6Address.orElse("<none>")); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index b0baae650e4..6462fb6f19d 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -71,47 +71,47 @@ public class GroupPreparer { } // There were some changes, so re-do the allocation with locks - try (Mutex lock = nodeRepository.lock(application)) { - try (Mutex allocationLock = nodeRepository.lockUnallocated()) { - NodeAllocation allocation = prepareAllocation(application, cluster, requestedNodes, surplusActiveNodes, - highestIndex, wantedGroups, allocationLock); - - if (nodeRepository.zone().getCloud().dynamicProvisioning()) { - Version osVersion = nodeRepository.osVersions().targetFor(NodeType.host).orElse(Version.emptyVersion); - List<ProvisionedHost> provisionedHosts = allocation.getFulfilledDockerDeficit() - .map(deficit -> hostProvisioner.get().provisionHosts(nodeRepository.database().getProvisionIndexes(deficit.getCount()), - deficit.getFlavor(), - application, - osVersion, - requestedNodes.isExclusive() ? HostSharing.exclusive : HostSharing.any)) - .orElseGet(List::of); - - // At this point we have started provisioning of the hosts, the first priority is to make sure that - // the returned hosts are added to the node-repo so that they are tracked by the provision maintainers - List<Node> hosts = provisionedHosts.stream() - .map(ProvisionedHost::generateHost) - .collect(Collectors.toList()); - nodeRepository.addNodes(hosts, Agent.application); - - // Offer the nodes on the newly provisioned hosts, this should be enough to cover the deficit - List<NodeCandidate> candidates = provisionedHosts.stream() - .map(host -> NodeCandidate.createNewExclusiveChild(host.generateNode(), - host.generateHost())) - .collect(Collectors.toList()); - allocation.offer(candidates); - } - - if (! allocation.fulfilled() && requestedNodes.canFail()) - throw new OutOfCapacityException((cluster.group().isPresent() ? "Out of capacity on " + cluster.group().get() :"") + - allocation.outOfCapacityDetails()); - - // Carry out and return allocation - nodeRepository.reserve(allocation.reservableNodes()); - nodeRepository.addDockerNodes(new LockedNodeList(allocation.newNodes(), allocationLock)); - List<Node> acceptedNodes = allocation.finalNodes(); - surplusActiveNodes.removeAll(acceptedNodes); - return acceptedNodes; + try (Mutex lock = nodeRepository.lock(application); + Mutex allocationLock = nodeRepository.lockUnallocated()) { + + NodeAllocation allocation = prepareAllocation(application, cluster, requestedNodes, surplusActiveNodes, + highestIndex, wantedGroups, allocationLock); + + if (nodeRepository.zone().getCloud().dynamicProvisioning()) { + Version osVersion = nodeRepository.osVersions().targetFor(NodeType.host).orElse(Version.emptyVersion); + List<ProvisionedHost> provisionedHosts = allocation.getFulfilledDockerDeficit() + .map(deficit -> hostProvisioner.get().provisionHosts(nodeRepository.database().getProvisionIndexes(deficit.getCount()), + deficit.getFlavor(), + application, + osVersion, + requestedNodes.isExclusive() ? HostSharing.exclusive : HostSharing.any)) + .orElseGet(List::of); + + // At this point we have started provisioning of the hosts, the first priority is to make sure that + // the returned hosts are added to the node-repo so that they are tracked by the provision maintainers + List<Node> hosts = provisionedHosts.stream() + .map(ProvisionedHost::generateHost) + .collect(Collectors.toList()); + nodeRepository.addNodes(hosts, Agent.application); + + // Offer the nodes on the newly provisioned hosts, this should be enough to cover the deficit + List<NodeCandidate> candidates = provisionedHosts.stream() + .map(host -> NodeCandidate.createNewExclusiveChild(host.generateNode(), + host.generateHost())) + .collect(Collectors.toList()); + allocation.offer(candidates); } + + if (! allocation.fulfilled() && requestedNodes.canFail()) + throw new OutOfCapacityException((cluster.group().isPresent() ? "Out of capacity on " + cluster.group().get() :"") + + allocation.outOfCapacityDetails()); + + // Carry out and return allocation + nodeRepository.reserve(allocation.reservableNodes()); + nodeRepository.addDockerNodes(new LockedNodeList(allocation.newNodes(), allocationLock)); + List<Node> acceptedNodes = allocation.finalNodes(); + surplusActiveNodes.removeAll(acceptedNodes); + return acceptedNodes; } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacity.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacity.java index 96053fdaa91..af3bde02421 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacity.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacity.java @@ -82,7 +82,11 @@ public class HostCapacity { * Number of free (not allocated) IP addresses assigned to the dockerhost. */ int freeIPs(Node dockerHost) { - return dockerHost.ipConfig().pool().findUnused(allNodes).size(); + if (dockerHost.type() == NodeType.host) { + return dockerHost.ipConfig().pool().eventuallyUnusedAddressCount(allNodes); + } else { + return dockerHost.ipConfig().pool().findUnusedIpAddresses(allNodes).size(); + } } /** diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java index f8231072a28..14937e6afeb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java @@ -363,11 +363,11 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { try { allocation = parent.get().ipConfig().pool().findAllocation(allNodes, nodeRepository.nameResolver()); if (allocation.isEmpty()) return new InvalidNodeCandidate(resources, freeParentCapacity, parent.get(), - "No IP addresses available on parent host"); + "No addresses available on parent host"); } catch (Exception e) { - log.warning("Failed allocating IP address on " + parent.get() +": " + Exceptions.toMessageString(e)); + log.warning("Failed allocating address on " + parent.get() +": " + Exceptions.toMessageString(e)); return new InvalidNodeCandidate(resources, freeParentCapacity, parent.get(), - "Failed when allocating IP address on host"); + "Failed when allocating address on host"); } Node node = Node.createDockerNode(allocation.get().addresses(), diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java index 22eec482b02..2833c4e11ba 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java @@ -209,12 +209,12 @@ public class DynamicProvisioningMaintainerTest { tester.maintainer.maintain(); assertTrue("No IP addresses written as DNS updates are failing", - provisioning.get().stream().allMatch(host -> host.ipConfig().pool().isEmpty())); + provisioning.get().stream().allMatch(host -> host.ipConfig().pool().getIpSet().isEmpty())); tester.hostProvisioner.without(Behaviour.failDnsUpdate); tester.maintainer.maintain(); assertTrue("IP addresses written as DNS updates are succeeding", - provisioning.get().stream().noneMatch(host -> host.ipConfig().pool().isEmpty())); + provisioning.get().stream().noneMatch(host -> host.ipConfig().pool().getIpSet().isEmpty())); } private static class DynamicProvisioningTester { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/node/IPTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/node/IPTest.java index fb9c1ad0e5a..8101405ad7f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/node/IPTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/node/IPTest.java @@ -86,8 +86,8 @@ public class IPTest { resolver.addReverseRecord("::2", "host1"); Optional<IP.Allocation> allocation = pool.findAllocation(emptyList, resolver); - assertEquals("::1", allocation.get().primary()); - assertFalse(allocation.get().secondary().isPresent()); + assertEquals(Optional.of("::1"), allocation.get().ipv6Address()); + assertFalse(allocation.get().ipv4Address().isPresent()); assertEquals("host3", allocation.get().hostname()); // Allocation fails if DNS record is missing @@ -105,16 +105,16 @@ public class IPTest { var pool = testPool(false); var allocation = pool.findAllocation(emptyList, resolver); assertFalse("Found allocation", allocation.isEmpty()); - assertEquals("127.0.0.1", allocation.get().primary()); - assertTrue("No secondary address", allocation.get().secondary().isEmpty()); + assertEquals(Optional.of("127.0.0.1"), allocation.get().ipv4Address()); + assertTrue("No IPv6 address", allocation.get().ipv6Address().isEmpty()); } @Test public void test_find_allocation_dual_stack() { IP.Pool pool = testPool(true); Optional<IP.Allocation> allocation = pool.findAllocation(emptyList, resolver); - assertEquals("::1", allocation.get().primary()); - assertEquals("127.0.0.2", allocation.get().secondary().get()); + assertEquals(Optional.of("::1"), allocation.get().ipv6Address()); + assertEquals("127.0.0.2", allocation.get().ipv4Address().get()); assertEquals("host3", allocation.get().hostname()); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacityTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacityTest.java index c6e89680e85..808770f42dc 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacityTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacityTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.LockedNodeList; import com.yahoo.vespa.hosted.provision.Node; +import com.yahoo.vespa.hosted.provision.node.Address; import com.yahoo.vespa.hosted.provision.node.IP; import org.junit.Before; import org.junit.Test; @@ -15,6 +16,8 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -32,8 +35,8 @@ public class HostCapacityTest { private HostCapacity capacity; private List<Node> nodes; private Node host1, host2, host3; - private final NodeResources resources1 = new NodeResources(1, 30, 20, 1.5); - private final NodeResources resources2 = new NodeResources(2, 40, 40, 0.5); + private final NodeResources dockerResources = new NodeResources(1, 30, 20, 1.5); + private final NodeResources docker2Resources = new NodeResources(2, 40, 40, 0.5); @Before public void setup() { @@ -48,15 +51,15 @@ public class HostCapacityTest { host3 = Node.create("host3", IP.Config.of(Set.of("::21"), generateIPs(22, 1), List.of()), "host3", nodeFlavors.getFlavorOrThrow("host"), NodeType.host).build(); // Add two containers to host1 - var nodeA = Node.createDockerNode(Set.of("::2"), "nodeA", "host1", resources1, NodeType.tenant).build(); - var nodeB = Node.createDockerNode(Set.of("::3"), "nodeB", "host1", resources1, NodeType.tenant).build(); + var nodeA = Node.createDockerNode(Set.of("::2"), "nodeA", "host1", dockerResources, NodeType.tenant).build(); + var nodeB = Node.createDockerNode(Set.of("::3"), "nodeB", "host1", dockerResources, NodeType.tenant).build(); // Add two containers to host 2 (same as host 1) - var nodeC = Node.createDockerNode(Set.of("::12"), "nodeC", "host2", resources1, NodeType.tenant).build(); - var nodeD = Node.createDockerNode(Set.of("::13"), "nodeD", "host2", resources1, NodeType.tenant).build(); + var nodeC = Node.createDockerNode(Set.of("::12"), "nodeC", "host2", dockerResources, NodeType.tenant).build(); + var nodeD = Node.createDockerNode(Set.of("::13"), "nodeD", "host2", dockerResources, NodeType.tenant).build(); // Add a larger container to host3 - var nodeE = Node.createDockerNode(Set.of("::22"), "nodeE", "host3", resources2, NodeType.tenant).build(); + var nodeE = Node.createDockerNode(Set.of("::22"), "nodeE", "host3", docker2Resources, NodeType.tenant).build(); // init docker host capacity nodes = new ArrayList<>(List.of(host1, host2, host3, nodeA, nodeB, nodeC, nodeD, nodeE)); @@ -65,19 +68,19 @@ public class HostCapacityTest { @Test public void hasCapacity() { - assertTrue(capacity.hasCapacity(host1, resources1)); - assertTrue(capacity.hasCapacity(host1, resources2)); - assertTrue(capacity.hasCapacity(host2, resources1)); - assertTrue(capacity.hasCapacity(host2, resources2)); - assertFalse(capacity.hasCapacity(host3, resources1)); // No ip available - assertFalse(capacity.hasCapacity(host3, resources2)); // No ip available + assertTrue(capacity.hasCapacity(host1, dockerResources)); + assertTrue(capacity.hasCapacity(host1, docker2Resources)); + assertTrue(capacity.hasCapacity(host2, dockerResources)); + assertTrue(capacity.hasCapacity(host2, docker2Resources)); + assertFalse(capacity.hasCapacity(host3, dockerResources)); // No ip available + assertFalse(capacity.hasCapacity(host3, docker2Resources)); // No ip available // Add a new node to host1 to deplete the memory resource - Node nodeF = Node.createDockerNode(Set.of("::6"), "nodeF", "host1", resources1, NodeType.tenant).build(); + Node nodeF = Node.createDockerNode(Set.of("::6"), "nodeF", "host1", dockerResources, NodeType.tenant).build(); nodes.add(nodeF); capacity = new HostCapacity(new LockedNodeList(nodes, () -> {}), hostResourcesCalculator); - assertFalse(capacity.hasCapacity(host1, resources1)); - assertFalse(capacity.hasCapacity(host1, resources2)); + assertFalse(capacity.hasCapacity(host1, dockerResources)); + assertFalse(capacity.hasCapacity(host1, docker2Resources)); } @Test @@ -112,19 +115,78 @@ public class HostCapacityTest { var nodeFlavors = FlavorConfigBuilder.createDummies("devhost", "container"); var devHost = Node.create("devhost", new IP.Config(Set.of("::1"), generateIPs(2, 10)), "devhost", nodeFlavors.getFlavorOrThrow("devhost"), NodeType.devhost).build(); - var cfg = Node.createDockerNode(Set.of("::2"), "cfg", "devhost", resources1, NodeType.config).build(); + var cfg = Node.createDockerNode(Set.of("::2"), "cfg", "devhost", dockerResources, NodeType.config).build(); var nodes = new ArrayList<>(List.of(cfg)); var capacity = new HostCapacity(new LockedNodeList(nodes, () -> {}), hostResourcesCalculator); - assertTrue(capacity.hasCapacity(devHost, resources1)); + assertTrue(capacity.hasCapacity(devHost, dockerResources)); - var container1 = Node.createDockerNode(Set.of("::3"), "container1", "devhost", resources1, NodeType.tenant).build(); + var container1 = Node.createDockerNode(Set.of("::3"), "container1", "devhost", dockerResources, NodeType.tenant).build(); nodes = new ArrayList<>(List.of(cfg, container1)); capacity = new HostCapacity(new LockedNodeList(nodes, () -> {}), hostResourcesCalculator); - assertFalse(capacity.hasCapacity(devHost, resources1)); + assertFalse(capacity.hasCapacity(devHost, dockerResources)); } + @Test + public void verifyCapacityFromAddresses() { + Node nodeA = Node.createDockerNode(Set.of("::2"), "nodeA", "host1", dockerResources, NodeType.tenant).build(); + Node nodeB = Node.createDockerNode(Set.of("::3"), "nodeB", "host1", dockerResources, NodeType.tenant).build(); + Node nodeC = Node.createDockerNode(Set.of("::4"), "nodeC", "host1", dockerResources, NodeType.tenant).build(); + + // host1 is a host with resources = 7-100-120-5 (7 vcpus, 100G memory, 120G disk, and 5Gbps), + // while nodeA-C have resources = dockerResources = 1-30-20-1.5 + + Node host1 = setupHostWithAdditionalHostnames("host1", "nodeA"); + // Allocating nodeA should be OK + assertTrue(hasCapacity(dockerResources, host1)); + // then, the second node lacks hostname address + assertFalse(hasCapacity(dockerResources, host1, nodeA)); + + host1 = setupHostWithAdditionalHostnames("host1", "nodeA", "nodeB"); + // Allocating nodeA and nodeB should be OK + assertTrue(hasCapacity(dockerResources, host1)); + assertTrue(hasCapacity(dockerResources, host1, nodeA)); + // but the third node lacks hostname address + assertFalse(hasCapacity(dockerResources, host1, nodeA, nodeB)); + + host1 = setupHostWithAdditionalHostnames("host1", "nodeA", "nodeB", "nodeC"); + // Allocating nodeA, nodeB, and nodeC should be OK + assertTrue(hasCapacity(dockerResources, host1)); + assertTrue(hasCapacity(dockerResources, host1, nodeA)); + assertTrue(hasCapacity(dockerResources, host1, nodeA, nodeB)); + // but the fourth node lacks hostname address + assertFalse(hasCapacity(dockerResources, host1, nodeA, nodeB, nodeC)); + + host1 = setupHostWithAdditionalHostnames("host1", "nodeA", "nodeB", "nodeC", "nodeD"); + // Allocating nodeA, nodeB, and nodeC should be OK + assertTrue(hasCapacity(dockerResources, host1)); + assertTrue(hasCapacity(dockerResources, host1, nodeA)); + assertTrue(hasCapacity(dockerResources, host1, nodeA, nodeB)); + // but the fourth lacks memory (host has 100G, while 4x30G = 120G + assertFalse(hasCapacity(dockerResources, host1, nodeA, nodeB, nodeC)); + } + + private Node setupHostWithAdditionalHostnames(String hostHostname, String... additionalHostnames) { + List<Address> addresses = Stream.of(additionalHostnames).map(Address::new).collect(Collectors.toList()); + + doAnswer(invocation -> ((Flavor)invocation.getArguments()[0]).resources()) + .when(hostResourcesCalculator).advertisedResourcesOf(any()); + + NodeFlavors nodeFlavors = FlavorConfigBuilder.createDummies( + "host", // 7-100-120-5 + "docker"); // 2- 40- 40-0.5 = docker2Resources + + return Node.create(hostHostname, IP.Config.of(Set.of("::1"), Set.of(), addresses), hostHostname, + nodeFlavors.getFlavorOrThrow("host"), NodeType.host).build(); + } + + private boolean hasCapacity(NodeResources requestedCapacity, Node host, Node... remainingNodes) { + List<Node> nodes = Stream.concat(Stream.of(host), Stream.of(remainingNodes)).collect(Collectors.toList()); + var capacity = new HostCapacity(new LockedNodeList(nodes, () -> {}), hostResourcesCalculator); + return capacity.hasCapacity(host, requestedCapacity); + } + private Set<String> generateIPs(int start, int count) { // Allow 4 containers Set<String> ipAddressPool = new LinkedHashSet<>(); |