summaryrefslogtreecommitdiffstats
path: root/node-repository
diff options
context:
space:
mode:
authorHÃ¥kon Hallingstad <hakon@verizonmedia.com>2020-11-21 18:23:07 +0100
committerGitHub <noreply@github.com>2020-11-21 18:23:07 +0100
commit3112575fc713fa41232537a47d660bdf0a82e85e (patch)
tree91da5486a1c27052669d747a21206fe9081862f1 /node-repository
parentdba9416bbd6a9039e46006364ab2356bdd0d1802 (diff)
parentb1211ae56e3d7414f38f130ba104d95da83ac261 (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')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java106
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java80
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacity.java6
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java6
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainerTest.java4
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/node/IPTest.java12
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/HostCapacityTest.java102
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<>();