summaryrefslogtreecommitdiffstats
path: root/node-repository/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'node-repository/src/main')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java34
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java14
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java22
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java36
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java11
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java4
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java112
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java39
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java128
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java23
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java41
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java27
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java25
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java150
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java17
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java31
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java10
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java5
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java13
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java7
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java13
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java21
-rw-r--r--node-repository/src/main/resources/configdefinitions/node-repository.def7
29 files changed, 345 insertions, 465 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 0f3a87ff585..80333dcd2d4 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
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.node.Allocation;
import com.yahoo.vespa.hosted.provision.node.Flavor;
import com.yahoo.vespa.hosted.provision.node.History;
@@ -29,7 +30,7 @@ public final class Node {
private final Flavor flavor;
private final Status status;
private final State state;
- private final Type type;
+ private final NodeType type;
/** Record of the last event of each type happening to this node */
private final History history;
@@ -38,20 +39,20 @@ public final class Node {
private Optional<Allocation> allocation;
/** Creates a node in the initial state (provisioned) */
- public static Node create(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, Type type) {
+ public static Node create(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, NodeType type) {
return new Node(openStackId, hostname, parentHostname, flavor, Status.initial(), State.provisioned,
Optional.empty(), History.empty(), type);
}
/** Do not use. Construct nodes by calling {@link NodeRepository#createNode} */
public Node(String openStackId, String hostname, Optional<String> parentHostname,
- Flavor flavor, Status status, State state, Allocation allocation, History history, Type type) {
+ Flavor flavor, Status status, State state, Allocation allocation, History history, NodeType type) {
this(openStackId, hostname, parentHostname, flavor, status, state, Optional.of(allocation), history, type);
}
public Node(String openStackId, String hostname, Optional<String> parentHostname,
Flavor flavor, Status status, State state, Optional<Allocation> allocation,
- History history, Type type) {
+ History history, NodeType type) {
Objects.requireNonNull(openStackId, "A node must have an openstack id");
Objects.requireNonNull(hostname, "A node must have a hostname");
Objects.requireNonNull(parentHostname, "A null parentHostname is not permitted.");
@@ -93,14 +94,14 @@ public final class Node {
/** Returns the flavor of this node */
public Flavor flavor() { return flavor; }
- /** Returns the known information about the nodes ephemeral status */
+ /** Returns the known information about the node's ephemeral status */
public Status status() { return status; }
/** Returns the current state of this node (in the node state machine) */
public State state() { return state; }
/** Returns the type of this node */
- public Type type() { return type; }
+ public NodeType type() { return type; }
/** Returns the current allocation of this, if any */
public Optional<Allocation> allocation() { return allocation; }
@@ -130,7 +131,7 @@ public final class Node {
return with(allocation.get().unretire());
}
- /** Returns a copy of this with the current generation set to generation */
+ /** Returns a copy of this with the current restart generation set to generation */
public Node withRestart(Generation generation) {
final Optional<Allocation> allocation = this.allocation;
if ( ! allocation.isPresent())
@@ -145,7 +146,7 @@ public final class Node {
}
/** Returns a node with the type assigned to the given value */
- public Node with(Type type) {
+ public Node with(NodeType type) {
return new Node(openStackId, hostname, parentHostname, flavor, status, state, allocation, history, type);
}
@@ -154,7 +155,7 @@ public final class Node {
return new Node(openStackId, hostname, parentHostname, flavor, status, state, allocation, history, type);
}
- /** Returns a copy of this with the current generation set to generation */
+ /** Returns a copy of this with the current reboot generation set to generation */
public Node withReboot(Generation generation) {
return new Node(openStackId, hostname, parentHostname, flavor, status.withReboot(generation), state,
allocation, history, type);
@@ -217,7 +218,7 @@ public final class Node {
public enum State {
- /** This node has been requested (from OpenStack) but is not yet read for use */
+ /** This node has been requested (from OpenStack) but is not yet ready for use */
provisioned,
/** This node is free and ready for use */
@@ -251,17 +252,4 @@ public final class Node {
}
}
- public enum Type {
-
- /** A host of a set of (docker) tenant nodes */
- host,
-
- /** Nodes running the shared proxy layer */
- proxy,
-
- /** A node to be assigned to a tenant to run application workloads */
- tenant
-
- }
-
}
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 606a5914e11..eee0eae4e83 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
@@ -5,6 +5,7 @@ import com.google.inject.Inject;
import com.yahoo.collections.ListMap;
import com.yahoo.component.AbstractComponent;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.transaction.Mutex;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.curator.Curator;
@@ -91,13 +92,22 @@ public class NodeRepository extends AbstractComponent {
}
/**
+ * Returns all nodes in any of the given states.
+ *
+ * @param inState the states to return nodes from. If no states are given, all nodes of the given type are returned
+ * @return the node, or empty if it was not found in any of the given states
+ */
+ public List<Node> getNodes(Node.State ... inState) {
+ return zkClient.getNodes(inState).stream().collect(Collectors.toList());
+ }
+ /**
* Finds and returns the nodes of the given type in any of the given states.
*
* @param type the node type to return
* @param inState the states to return nodes from. If no states are given, all nodes of the given type are returned
* @return the node, or empty if it was not found in any of the given states
*/
- public List<Node> getNodes(Node.Type type, Node.State ... inState) {
+ public List<Node> getNodes(NodeType type, Node.State ... inState) {
return zkClient.getNodes(inState).stream().filter(node -> node.type().equals(type)).collect(Collectors.toList());
}
public List<Node> getNodes(ApplicationId id, Node.State ... inState) { return zkClient.getNodes(id, inState); }
@@ -114,7 +124,7 @@ public class NodeRepository extends AbstractComponent {
/** Creates a new node object, without adding it to the node repo */
public Node createNode(String openStackId, String hostname, Optional<String> parentHostname,
- Flavor flavor, Node.Type type) {
+ Flavor flavor, NodeType type) {
return Node.create(openStackId, hostname, parentHostname, flavor, type);
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java
index 14305692664..422eee820db 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java
@@ -95,7 +95,7 @@ public class PopulateClient {
Node.State.active,
Optional.empty() /* Allocation */,
History.empty(),
- Node.Type.tenant) // History
+ NodeType.tenant) // History
.allocate(
ApplicationId.from(
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
index ce7ae429c40..32478473111 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java
@@ -4,9 +4,9 @@ package com.yahoo.vespa.hosted.provision.maintenance;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Deployer;
import com.yahoo.config.provision.Deployment;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
-import com.yahoo.yolean.Exceptions;
import java.time.Duration;
import java.util.Optional;
@@ -35,7 +35,7 @@ public class ApplicationMaintainer extends Maintainer {
@Override
protected void maintain() {
Set<ApplicationId> applications =
- nodeRepository().getNodes(Node.Type.tenant, Node.State.active).stream().map(node -> node.allocation().get().owner()).collect(Collectors.toSet());
+ nodeRepository().getNodes(Node.State.active).stream().map(node -> node.allocation().get().owner()).collect(Collectors.toSet());
for (ApplicationId application : applications) {
try {
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java
index 495ff3b756f..5b6cc3b11a3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java
@@ -1,6 +1,7 @@
// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.maintenance;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.node.History;
@@ -47,7 +48,7 @@ public abstract class Expirer extends Maintainer {
@Override
protected void maintain() {
List<Node> expired = new ArrayList<>();
- for (Node node : nodeRepository().getNodes(Node.Type.tenant, fromState)) {
+ for (Node node : nodeRepository().getNodes(fromState)) {
Optional<History.Event> event = node.history().event(eventType);
if (event.isPresent() && event.get().at().plus(expiryTime).isBefore(clock.instant()))
expired.add(node);
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java
index 4b16c0947cb..f194d9d53fd 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.maintenance;
import com.yahoo.config.provision.Deployer;
import com.yahoo.config.provision.Deployment;
import com.yahoo.config.provision.HostLivenessTracker;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.applicationmodel.ApplicationInstance;
import com.yahoo.vespa.applicationmodel.ServiceCluster;
@@ -86,7 +87,7 @@ public class NodeFailer extends Maintainer {
// Active nodes
for (Node node : determineActiveNodeDownStatus()) {
Instant graceTimeEnd = node.history().event(History.Event.Type.down).get().at().plus(downTimeLimit);
- if (graceTimeEnd.isBefore(clock.instant()) && ! applicationSuspended(node))
+ if (graceTimeEnd.isBefore(clock.instant()) && ! applicationSuspended(node) && failAllowedFor(node.type()))
failActive(node);
}
}
@@ -95,7 +96,7 @@ public class NodeFailer extends Maintainer {
// Update node last request events through ZooKeeper to collect request to all config servers.
// We do this here ("lazily") to avoid writing to zk for each config request.
try (Mutex lock = nodeRepository().lockUnallocated()) {
- for (Node node : nodeRepository().getNodes(Node.Type.tenant, Node.State.ready)) {
+ for (Node node : nodeRepository().getNodes(Node.State.ready)) {
Optional<Instant> lastLocalRequest = hostLivenessTracker.lastRequestFrom(node.hostname());
if ( ! lastLocalRequest.isPresent()) continue;
@@ -118,7 +119,7 @@ public class NodeFailer extends Maintainer {
// Add 10 minutes to the down time limit to allow nodes to make a request that infrequently.
Instant oldestAcceptableRequestTime = clock.instant().minus(downTimeLimit).minus(nodeRequestInterval);
- return nodeRepository().getNodes(Node.Type.tenant, Node.State.ready).stream()
+ return nodeRepository().getNodes(Node.State.ready).stream()
.filter(node -> wasMadeReadyBefore(oldestAcceptableRequestTime, node))
.filter(node -> ! hasRecordedRequestAfter(oldestAcceptableRequestTime, node))
.collect(Collectors.toList());
@@ -137,7 +138,7 @@ public class NodeFailer extends Maintainer {
}
private List<Node> readyNodesWithHardwareFailure() {
- return nodeRepository().getNodes(Node.Type.tenant, Node.State.ready).stream()
+ return nodeRepository().getNodes(Node.State.ready).stream()
.filter(node -> node.status().hardwareFailure().isPresent())
.collect(Collectors.toList());
}
@@ -153,6 +154,17 @@ public class NodeFailer extends Maintainer {
}
/**
+ * We can attempt to fail any number of *tenant* nodes because the operation will not be effected unless
+ * the node is replaced.
+ * However, nodes of other types are not replaced (because all of the type are used by a single application),
+ * so we only allow one to be in failed at any point in time to protect against runaway failing.
+ */
+ private boolean failAllowedFor(NodeType nodeType) {
+ if (nodeType == NodeType.tenant) return true;
+ return nodeRepository().getNodes(nodeType, Node.State.failed).size() == 0;
+ }
+
+ /**
* If the node is positively DOWN, and there is no "down" history record, we add it.
* If the node is positively UP we remove any "down" history record.
*
@@ -164,7 +176,7 @@ public class NodeFailer extends Maintainer {
for (ServiceCluster<ServiceMonitorStatus> cluster : application.serviceClusters()) {
for (ServiceInstance<ServiceMonitorStatus> service : cluster.serviceInstances()) {
Optional<Node> node = nodeRepository().getNode(service.hostName().s(), Node.State.active);
- if ( ! node.isPresent()) continue; // we also get status from infrastructure nodes, which are not in the repo
+ if ( ! node.isPresent()) continue; // we also get status from infrastructure nodes, which are not in the repo. TODO: remove when proxy nodes are in node repo everywhere
if (service.serviceStatus().equals(ServiceMonitorStatus.DOWN))
downNodes.add(recordAsDown(node.get()));
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
index 3e6881b912b..fa3bdeea776 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
@@ -105,7 +105,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent {
redeployFrequency = Duration.ofMinutes(30);
zooKeeperAccessMaintenanceInterval = Duration.ofSeconds(10);
reservationExpiry = Duration.ofMinutes(10); // Need to be long enough for deployment to be finished for all config model versions
- inactiveExpiry = Duration.ofMinutes(1);
+ inactiveExpiry = Duration.ofSeconds(2); // support interactive wipe start over
retiredExpiry = Duration.ofMinutes(1);
failedExpiry = Duration.ofMinutes(10);
dirtyExpiry = Duration.ofMinutes(30);
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java
index 085e3e4dac8..d558555512f 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java
@@ -1,5 +1,6 @@
package com.yahoo.vespa.hosted.provision.maintenance;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
@@ -31,9 +32,9 @@ public class ZooKeeperAccessMaintainer extends Maintainer {
protected void maintain() {
StringBuilder hostList = new StringBuilder();
- for (Node node : nodeRepository().getNodes(Node.Type.tenant))
+ for (Node node : nodeRepository().getNodes(NodeType.tenant))
hostList.append(node.hostname()).append(",");
- for (Node node : nodeRepository().getNodes(Node.Type.proxy))
+ for (Node node : nodeRepository().getNodes(NodeType.proxy))
hostList.append(node.hostname()).append(",");
for (String hostPort : curator.connectionSpec().split(","))
hostList.append(hostPort.split(":")[0]).append(",");
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java
index b1e0df929f4..18f4765fa03 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.provision.monitoring;
import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.jdisc.Metric;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.provision.Node;
@@ -50,10 +51,11 @@ public class ProvisionMetrics extends AbstractComponent {
log.log(LogLevel.DEBUG, "Running provision metrics task");
try {
for (Node.State state : Node.State.values())
- metric.set("hostedVespa." + state.name() + "Hosts", nodeRepository.getNodes(Node.Type.tenant, state).size(), null);
+ metric.set("hostedVespa." + state.name() + "Hosts", nodeRepository.getNodes(NodeType.tenant, state).size(), null);
} catch (RuntimeException e) {
log.log(LogLevel.INFO, "Failed gathering metrics data: " + e.getMessage());
}
}
}
+
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java
index 0893ca75f92..1039beea7c0 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java
@@ -17,6 +17,7 @@ public class Flavor {
private final String name;
private final int cost;
+ private final boolean isStock;
private final Type type;
private final double minCpuCores;
private final double minMainMemoryAvailableGb;
@@ -32,6 +33,7 @@ public class Flavor {
this.name = flavorConfig.name();
this.replacesFlavors = new ArrayList<>();
this.cost = flavorConfig.cost();
+ this.isStock = flavorConfig.stock();
this.type = Type.valueOf(flavorConfig.environment());
this.minCpuCores = flavorConfig.minCpuCores();
this.minMainMemoryAvailableGb = flavorConfig.minMainMemoryAvailableGb();
@@ -39,6 +41,7 @@ public class Flavor {
this.description = flavorConfig.description();
}
+ /** Returns the unique identity of this flavor */
public String name() { return name; }
/**
@@ -47,26 +50,18 @@ public class Flavor {
* @return Monthly cost in USD
*/
public int cost() { return cost; }
+
+ public boolean isStock() { return isStock; }
- public double getMinMainMemoryAvailableGb() {
- return minMainMemoryAvailableGb;
- }
+ public double getMinMainMemoryAvailableGb() { return minMainMemoryAvailableGb; }
- public double getMinDiskAvailableGb() {
- return minDiskAvailableGb;
- }
+ public double getMinDiskAvailableGb() { return minDiskAvailableGb; }
- public double getMinCpuCores() {
- return minCpuCores;
- }
+ public double getMinCpuCores() { return minCpuCores; }
- public String getDescription() {
- return description;
- }
+ public String getDescription() { return description; }
- public Type getType() {
- return type;
- }
+ public Type getType() { return type; }
/**
* Returns the canonical name of this flavor - which is the name which should be used as an interface to users.
@@ -78,11 +73,16 @@ public class Flavor {
*
* The logic is that we can use this to capture the gritty details of configurations in exact flavor names
* but also encourage users to refer to them by a common name by letting such flavor variants declare that they
- * replace the canonical name we want. However, if a node replaces multiple names, it means that a former
- * flavor distinction has become obsolete so this name becomes one of the canonical names users should refer to.
+ * replace the canonical name we want. However, if a node replaces multiple names, we have no basis for choosing one
+ * of them as the canonical, so we return the current as canonical.
*/
public String canonicalName() {
- return replacesFlavors.size() == 1 ? replacesFlavors.get(0).canonicalName() : name;
+ return isCanonical() ? name : replacesFlavors.get(0).canonicalName();
+ }
+
+ /** Returns whether this is a canonical flavor */
+ public boolean isCanonical() {
+ return replacesFlavors.size() != 1;
}
/**
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java
index 7fb3abb5b8e..d85347847ae 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java
@@ -6,6 +6,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.TenantName;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
@@ -279,15 +280,15 @@ public class NodeSerializer {
throw new IllegalArgumentException("Serialized form of '" + agent + "' not defined");
}
- private Node.Type nodeTypeFromString(String typeString) {
+ private NodeType nodeTypeFromString(String typeString) {
switch (typeString) {
- case "tenant" : return Node.Type.tenant;
- case "host" : return Node.Type.host;
- case "proxy" : return Node.Type.proxy;
+ case "tenant" : return NodeType.tenant;
+ case "host" : return NodeType.host;
+ case "proxy" : return NodeType.proxy;
default : throw new IllegalArgumentException("Unknown node type '" + typeString + "'");
}
}
- private String toString(Node.Type type) {
+ private String toString(NodeType type) {
switch (type) {
case tenant: return "tenant";
case host: return "host";
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
index 6665833c1a2..a759a8fca37 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java
@@ -31,7 +31,7 @@ public class CapacityPolicies {
switch(zone.environment()) {
case dev : case test : return 1;
- case perf : return Math.min(requestedCapacity.nodeCount(), 10); // TODO: Decrease to 3 when isRequired is implemented
+ case perf : return Math.min(requestedCapacity.nodeCount(), 3);
case staging: return requestedNodes <= 1 ? requestedNodes : Math.max(2, requestedNodes / 10);
case prod : return ensureRedundancy(requestedCapacity.nodeCount());
default : throw new IllegalArgumentException("Unsupported environment " + zone.environment());
@@ -53,7 +53,7 @@ public class CapacityPolicies {
/**
* Throw if the node count is 1
-
+ *
* @return the argument node count
* @throws IllegalArgumentException if only one node is requested
*/
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 583111b9d65..ea205a15040 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
@@ -10,7 +10,6 @@ import com.yahoo.lang.MutableInteger;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
-import com.yahoo.vespa.hosted.provision.node.Flavor;
import java.time.Clock;
import java.util.ArrayList;
@@ -46,8 +45,7 @@ class GroupPreparer {
*
* @param application the application we are allocating to
* @param cluster the cluster and group we are allocating to
- * @param nodeCount the desired number of nodes to return
- * @param flavor the desired flavor of those nodes
+ * @param requestedNodes a specification of the requested nodes
* @param surplusActiveNodes currently active nodes which are available to be assigned to this group.
* This method will remove from this list if it finds it needs additional nodes
* @param highestIndex the current highest node index among all active nodes in this cluster.
@@ -57,58 +55,71 @@ class GroupPreparer {
// Note: This operation may make persisted changes to the set of reserved and inactive nodes,
// but it may not change the set of active nodes, as the active nodes must stay in sync with the
// active config model which is changed on activate
- public List<Node> prepare(ApplicationId application, ClusterSpec cluster, int nodeCount, Flavor flavor, List<Node> surplusActiveNodes, MutableInteger highestIndex) {
+ public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes,
+ List<Node> surplusActiveNodes, MutableInteger highestIndex) {
try (Mutex lock = nodeRepository.lock(application)) {
- NodeList nodeList = new NodeList(application, cluster, nodeCount, flavor, highestIndex);
+ NodeList nodeList = new NodeList(application, cluster, requestedNodes, highestIndex);
// Use active nodes
nodeList.offer(nodeRepository.getNodes(application, Node.State.active), !canChangeGroup);
- if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes);
+ if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
// Use active nodes from other groups that will otherwise be retired
- List<Node> accepted = nodeList.offer(sortNodeListByCost(surplusActiveNodes), canChangeGroup);
+ List<Node> accepted = nodeList.offer(prioritizeNodes(surplusActiveNodes, requestedNodes), canChangeGroup);
surplusActiveNodes.removeAll(accepted);
- if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes);
+ if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
// Use previously reserved nodes
nodeList.offer(nodeRepository.getNodes(application, Node.State.reserved), !canChangeGroup);
- if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes);
+ if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
// Use inactive nodes
- accepted = nodeList.offer(sortNodeListByCost(nodeRepository.getNodes(application, Node.State.inactive)), !canChangeGroup);
+ accepted = nodeList.offer(prioritizeNodes(nodeRepository.getNodes(application, Node.State.inactive), requestedNodes), !canChangeGroup);
nodeList.update(nodeRepository.reserve(accepted));
- if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes);
+ if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes);
// Use new, ready nodes. Lock ready pool to ensure that nodes are not grabbed by others.
try (Mutex readyLock = nodeRepository.lockUnallocated()) {
- List<Node> readyNodes = nodeRepository.getNodes(Node.Type.tenant, Node.State.ready);
- accepted = nodeList.offer(stripeOverHosts(sortNodeListByCost(readyNodes)), !canChangeGroup);
+ List<Node> readyNodes = nodeRepository.getNodes(requestedNodes.type(), Node.State.ready);
+ accepted = nodeList.offer(stripeOverHosts(prioritizeNodes(readyNodes, requestedNodes)), !canChangeGroup);
nodeList.update(nodeRepository.reserve(accepted));
}
- if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes);
- if (nodeList.whatAboutUsingRetiredNodes()) {
- throw new OutOfCapacityException("Could not satisfy request for " + nodeCount +
- " nodes of " + flavor + " for " + cluster +
+ if (nodeList.fullfilled()) return nodeList.finalNodes(surplusActiveNodes);
+
+ // Could not be fulfilled
+ if (nodeList.wouldBeFulfilledWithRetiredNodes())
+ throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster +
" because we want to retire existing nodes.");
- }
- if (nodeList.whatAboutUsingVMs()) {
- throw new OutOfCapacityException("Could not satisfy request for " + nodeCount +
- " nodes of " + flavor + " for " + cluster +
+ else if (nodeList.wouldBeFulfilledWithClashingParentHost())
+ throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster +
" because too many have same parentHost.");
- }
- throw new OutOfCapacityException("Could not satisfy request for " + nodeCount +
- " nodes of " + flavor + " for " + cluster + ".");
+ else
+ throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster + ".");
}
}
- /** Sort nodes according to their cost, and if the cost is equal, sort by hostname (to get stable tests) */
- private List<Node> sortNodeListByCost(List<Node> nodeList) {
- Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start()
- .compare(n1.flavor().cost(), n2.flavor().cost())
- .compare(n1.hostname(), n2.hostname())
- .result()
- );
+ /**
+ * Returns the node list in prioritized order, where the nodes we would most prefer the application
+ * to use comes first
+ */
+ private List<Node> prioritizeNodes(List<Node> nodeList, NodeSpec nodeSpec) {
+ if ( nodeSpec.specifiesNonStockFlavor()) { // sort by exact before inexact flavor match, increasing cost, hostname
+ Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start()
+ .compareTrueFirst(nodeSpec.matchesExactly(n1.flavor()), nodeSpec.matchesExactly(n2.flavor()))
+ .compare(n1.flavor().cost(), n2.flavor().cost())
+ .compare(n1.hostname(), n2.hostname())
+ .result()
+ );
+ }
+ else { // sort by increasing cost, hostname
+ Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start()
+ .compareTrueFirst(nodeSpec.matchesExactly(n1.flavor()), nodeSpec.matchesExactly(n1.flavor()))
+ .compare(n1.flavor().cost(), n2.flavor().cost())
+ .compare(n1.hostname(), n2.hostname())
+ .result()
+ );
+ }
return nodeList;
}
@@ -159,11 +170,8 @@ class GroupPreparer {
/** The cluster this list is for */
private final ClusterSpec cluster;
- /** The requested capacity of the list */
- private final int requestedNodes;
-
- /** The requested node flavor */
- private final Flavor requestedFlavor;
+ /** The requested nodes of this list */
+ private final NodeSpec requestedNodes;
/** The nodes this has accepted so far */
private final Set<Node> nodes = new LinkedHashSet<>();
@@ -183,11 +191,10 @@ class GroupPreparer {
/** The next membership index to assign to a new node */
private MutableInteger highestIndex;
- public NodeList(ApplicationId application, ClusterSpec cluster, int requestedNodes, Flavor requestedFlavor, MutableInteger highestIndex) {
+ public NodeList(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, MutableInteger highestIndex) {
this.application = application;
this.cluster = cluster;
this.requestedNodes = requestedNodes;
- this.requestedFlavor = requestedFlavor;
this.highestIndex = highestIndex;
}
@@ -210,7 +217,7 @@ class GroupPreparer {
ClusterMembership membership = offered.allocation().get().membership();
if ( ! offered.allocation().get().owner().equals(application)) continue; // wrong application
if ( ! membership.cluster().equalsIgnoringGroup(cluster)) continue; // wrong cluster id/type
- if ( (! canChangeGroup || satisfied()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it
+ if ((! canChangeGroup || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it
if ( offered.allocation().get().isRemovable()) continue; // don't accept; causes removal
if ( indexes.contains(membership.index())) continue; // duplicate index (just to be sure)
@@ -218,10 +225,10 @@ class GroupPreparer {
if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) wantToRetireNode = true;
if ( !hasCompatibleFlavor(offered)) wantToRetireNode = true;
- if ( ( !satisfied() && hasCompatibleFlavor(offered)) || acceptToRetire(offered) )
+ if ((!saturated() && hasCompatibleFlavor(offered)) || acceptToRetire(offered) )
accepted.add(acceptNode(offered, wantToRetireNode));
}
- else if (! satisfied() && hasCompatibleFlavor(offered)) {
+ else if (! saturated() && hasCompatibleFlavor(offered)) {
if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) {
++rejectedWithClashingParentHost;
continue;
@@ -268,7 +275,7 @@ class GroupPreparer {
}
private boolean hasCompatibleFlavor(Node node) {
- return node.flavor().satisfies(requestedFlavor);
+ return requestedNodes.isCompatible(node.flavor());
}
/** Updates the state of some existing nodes in this list by replacing them by id with the given instances. */
@@ -305,17 +312,22 @@ class GroupPreparer {
return node.with(node.allocation().get().with(membership));
}
- /** Returns true if we have accepted at least the requested number of nodes of the requested flavor */
- public boolean satisfied() {
- return acceptedOfRequestedFlavor >= requestedNodes;
+ /** Returns true if no more nodes are needed in this list */
+ public boolean saturated() {
+ return requestedNodes.saturatedBy(acceptedOfRequestedFlavor);
+ }
+
+ /** Returns true if the content of this list is sufficient to meet the request */
+ public boolean fullfilled() {
+ return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor);
}
- public boolean whatAboutUsingRetiredNodes() {
- return acceptedOfRequestedFlavor + wasRetiredJustNow >= requestedNodes;
+ public boolean wouldBeFulfilledWithRetiredNodes() {
+ return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + wasRetiredJustNow);
}
- public boolean whatAboutUsingVMs() {
- return acceptedOfRequestedFlavor + rejectedWithClashingParentHost >= requestedNodes;
+ public boolean wouldBeFulfilledWithClashingParentHost() {
+ return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + rejectedWithClashingParentHost);
}
/**
@@ -329,7 +341,7 @@ class GroupPreparer {
*/
public List<Node> finalNodes(List<Node> surplusNodes) {
long currentRetired = nodes.stream().filter(node -> node.allocation().get().membership().retired()).count();
- long surplus = nodes.size() - requestedNodes - currentRetired;
+ long surplus = requestedNodes.surplusGiven(nodes.size()) - currentRetired;
List<Node> changedNodes = new ArrayList<>();
if (surplus > 0) { // retire until surplus is 0, prefer to retire higher indexes to minimize redistribution
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
index 5924e3fcb18..8e6c9f0c4ee 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java
@@ -7,6 +7,7 @@ import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostFilter;
import com.yahoo.config.provision.HostSpec;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.ProvisionLogger;
import com.yahoo.config.provision.Provisioner;
import com.yahoo.config.provision.Zone;
@@ -61,19 +62,33 @@ public class NodeRepositoryProvisioner implements Provisioner {
* The nodes are ordered by increasing index number.
*/
@Override
- public List<HostSpec> prepare(ApplicationId application, ClusterSpec cluster, Capacity requestedCapacity, int groups, ProvisionLogger logger) {
- log.log(LogLevel.DEBUG, () -> "Received deploy prepare request for " + requestedCapacity + " in " +
- groups + " groups for application " + application + ", cluster " + cluster);
-
- Flavor flavor = capacityPolicies.decideFlavor(requestedCapacity, cluster);
- int nodeCount = capacityPolicies.decideSize(requestedCapacity);
- int effectiveGroups = groups > nodeCount ? nodeCount : groups; // cannot have more groups than nodes
+ public List<HostSpec> prepare(ApplicationId application, ClusterSpec cluster, Capacity requestedCapacity,
+ int wantedGroups, ProvisionLogger logger) {
+ if (cluster.group().isPresent()) throw new IllegalArgumentException("Node requests cannot specify a group");
+ if (requestedCapacity.nodeCount() > 0 && requestedCapacity.nodeCount() % wantedGroups != 0)
+ throw new IllegalArgumentException("Requested " + requestedCapacity.nodeCount() + " nodes in " + wantedGroups + " groups, " +
+ "which doesn't allow the nodes to be divided evenly into groups");
- if (zone.environment().isManuallyDeployed() && nodeCount < requestedCapacity.nodeCount())
- logger.log(Level.WARNING, "Requested " + requestedCapacity.nodeCount() + " nodes for " + cluster +
- ", downscaling to " + nodeCount + " nodes in " + zone.environment());
-
- return asSortedHosts(preparer.prepare(application, cluster, nodeCount, flavor, effectiveGroups));
+ log.log(LogLevel.DEBUG, () -> "Received deploy prepare request for " + requestedCapacity + " in " +
+ wantedGroups + " groups for application " + application + ", cluster " + cluster);
+
+ int effectiveGroups;
+ NodeSpec requestedNodes;
+ if ( requestedCapacity.type() == NodeType.tenant) {
+ int nodeCount = capacityPolicies.decideSize(requestedCapacity);
+ if (zone.environment().isManuallyDeployed() && nodeCount < requestedCapacity.nodeCount())
+ logger.log(Level.INFO, "Requested " + requestedCapacity.nodeCount() + " nodes for " + cluster +
+ ", downscaling to " + nodeCount + " nodes in " + zone.environment());
+ Flavor flavor = capacityPolicies.decideFlavor(requestedCapacity, cluster);
+ effectiveGroups = wantedGroups > nodeCount ? nodeCount : wantedGroups; // cannot have more groups than nodes
+ requestedNodes = NodeSpec.from(nodeCount, flavor);
+ }
+ else {
+ requestedNodes = NodeSpec.from(requestedCapacity.type());
+ effectiveGroups = 1; // type request with multiple groups is not supported
+ }
+
+ return asSortedHosts(preparer.prepare(application, cluster, requestedNodes, effectiveGroups));
}
@Override
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
new file mode 100644
index 00000000000..2ce364daa07
--- /dev/null
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java
@@ -0,0 +1,128 @@
+package com.yahoo.vespa.hosted.provision.provisioning;
+
+import com.yahoo.config.provision.NodeType;
+import com.yahoo.vespa.hosted.provision.node.Flavor;
+
+import java.util.Objects;
+
+/**
+ * A specification of a set of nodes.
+ * This reflects that nodes can be requested either by count and flavor or by type,
+ * and encapsulates the differences in logic between these two cases.
+ *
+ * @author bratseth
+ */
+public interface NodeSpec {
+
+ /** The node type this requests */
+ NodeType type();
+
+ /** Returns whether the given flavor is compatible with this spec */
+ boolean isCompatible(Flavor flavor);
+
+ /** Returns whether the given flavor is exactly specified by this node spec */
+ boolean matchesExactly(Flavor flavor);
+
+ /** Returns whether this requests a non-stock flavor */
+ boolean specifiesNonStockFlavor();
+
+ /** Returns whether the given node count is sufficient to consider this spec fulfilled to the maximum amount */
+ boolean saturatedBy(int count);
+
+ /** Returns whether the given node count is sufficient to fulfill this spec */
+ boolean fulfilledBy(int count);
+
+ /** Returns the amount the given count is above the minimum amount needed to fulfill this request */
+ int surplusGiven(int count);
+
+ /** Returns a specification of a fraction of all the nodes of this. It is assumed the argument is a valid divisor. */
+ NodeSpec fraction(int divisor);
+
+ static NodeSpec from(int nodeCount, Flavor flavor) {
+ return new CountNodeSpec(nodeCount, flavor);
+ }
+
+ static NodeSpec from(NodeType type) {
+ return new TypeNodeSpec(type);
+ }
+
+ /** A node spec specifying a node count and a flavor */
+ class CountNodeSpec implements NodeSpec {
+
+ private final int count;
+ private final Flavor flavor;
+
+ public CountNodeSpec(int count, Flavor flavor) {
+ Objects.requireNonNull(flavor, "A flavor must be specified");
+ this.count = count;
+ this.flavor = flavor;
+ }
+
+ @Override
+ public NodeType type() { return NodeType.tenant; }
+
+ @Override
+ public boolean isCompatible(Flavor flavor) { return flavor.satisfies(this.flavor); }
+
+ @Override
+ public boolean matchesExactly(Flavor flavor) { return flavor.equals(this.flavor); }
+
+ @Override
+ public boolean specifiesNonStockFlavor() { return ! flavor.isStock(); }
+
+ @Override
+ public boolean fulfilledBy(int count) { return count >= this.count; }
+
+ @Override
+ public boolean saturatedBy(int count) { return fulfilledBy(count); } // min=max for count specs
+
+ @Override
+ public int surplusGiven(int count) { return count - this.count; }
+
+ @Override
+ public NodeSpec fraction(int divisor) { return new CountNodeSpec(count/divisor, flavor); }
+
+ @Override
+ public String toString() { return "request for " + count + " nodes of " + flavor; }
+
+ }
+
+ /** A node spec specifying a node type. This will accept all nodes of this type. */
+ class TypeNodeSpec implements NodeSpec {
+
+ private final NodeType type;
+
+ public TypeNodeSpec(NodeType type) {
+ this.type = type;
+ }
+
+ @Override
+ public NodeType type() { return type; }
+
+ @Override
+ public boolean isCompatible(Flavor flavor) { return true; }
+
+ @Override
+ public boolean matchesExactly(Flavor flavor) { return false; }
+
+ @Override
+ public boolean specifiesNonStockFlavor() { return false; }
+
+ @Override
+ public boolean fulfilledBy(int count) { return true; }
+
+ @Override
+ public boolean saturatedBy(int count) { return false; }
+
+ @Override
+ public int surplusGiven(int count) { return 0; }
+
+ @Override
+ public NodeSpec fraction(int divisor) { return this; }
+
+ @Override
+ public String toString() { return "request for all nodes of type '" + type + "'"; }
+
+ }
+
+}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
index dfb06321233..d34a91aec77 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java
@@ -40,28 +40,15 @@ class Preparer {
// Note: This operation may make persisted changes to the set of reserved and inactive nodes,
// but it may not change the set of active nodes, as the active nodes must stay in sync with the
// active config model which is changed on activate
- public List<Node> prepare(ApplicationId application, ClusterSpec cluster, int nodes, Flavor flavor, int wantedGroups) {
- // TODO: Encode actual assumptions as we have logic that depends on them:
- // - Don't allow a cluster spec specifying an explicit group (and then remove the "targetgroup" parameter to moveToActiveGroup
- // - Change group ids to be a 0-based integer index
- if (cluster.group().isPresent() && wantedGroups > 1)
- throw new IllegalArgumentException("Cannot specify both a particular group and request multiple groups");
- if (nodes > 0 && nodes % wantedGroups != 0)
- throw new IllegalArgumentException("Requested " + nodes + " nodes in " + wantedGroups + " groups, " +
- "which doesn't allow the nodes to be divided evenly into groups");
-
- // no group -> this asks for the entire cluster -> we are free to remove groups we won't need
- List<Node> surplusNodes =
- cluster.group().isPresent() ? new ArrayList<>() : findNodesInRemovableGroups(application, cluster, wantedGroups);
+ public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, int wantedGroups) {
+ List<Node> surplusNodes = findNodesInRemovableGroups(application, cluster, wantedGroups);
MutableInteger highestIndex = new MutableInteger(findHighestIndex(application, cluster));
List<Node> acceptedNodes = new ArrayList<>();
for (int groupIndex = 0; groupIndex < wantedGroups; groupIndex++) {
- // Generated groups always have contiguous indexes starting from 0
- ClusterSpec clusterGroup =
- cluster.group().isPresent() ? cluster : cluster.changeGroup(Optional.of(ClusterSpec.Group.from(groupIndex)));
-
- List<Node> accepted = groupPreparer.prepare(application, clusterGroup, nodes/wantedGroups, flavor, surplusNodes, highestIndex);
+ ClusterSpec clusterGroup = cluster.changeGroup(Optional.of(ClusterSpec.Group.from(groupIndex)));
+ List<Node> accepted = groupPreparer.prepare(application, clusterGroup,
+ requestedNodes.fraction(wantedGroups), surplusNodes, highestIndex);
replace(acceptedNodes, accepted);
}
moveToActiveGroup(surplusNodes, wantedGroups, cluster.group());
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java
deleted file mode 100644
index a501e7f5a0a..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import java.util.List;
-
-/**
- * Represents the JSON reply for getContainersForHost.
- * Serialized by jackson, and therefore uses public fields to avoid writing cruft.
- *
- * @author tonytv
- */
-public class ContainersForHost {
-
- public List<DockerContainer> dockerContainers;
-
- public static class DockerContainer {
- public String containerHostname;
- public String dockerImage;
- public String nodeState;
- public long wantedRestartGeneration;
- public long currentRestartGeneration;
-
- public DockerContainer(
- String containerHostname,
- String dockerImage,
- String nodeState,
- long wantedRestartGeneration,
- long currentRestartGeneration) {
- this.containerHostname = containerHostname;
- this.dockerImage = dockerImage;
- this.nodeState = nodeState;
- this.wantedRestartGeneration = wantedRestartGeneration;
- this.currentRestartGeneration = currentRestartGeneration;
- }
- }
-
- public ContainersForHost(List<DockerContainer> dockerContainers) {
- this.dockerContainers = dockerContainers;
- }
-
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java
deleted file mode 100644
index 81211f978f7..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-/**
- * Value class used to automatically convert to/from JSON.
- *
- * @author Oyvind Gronnesby
- */
-class HostInfo {
-
- public String hostname;
- public String openStackId;
- public String flavor;
-
- public static HostInfo createHostInfo(String hostname, String openStackId, String flavor) {
- HostInfo hostInfo = new HostInfo();
- hostInfo.hostname = hostname;
- hostInfo.openStackId = openStackId;
- hostInfo.flavor = flavor;
- return hostInfo;
- }
-
- public String toString(){
- return String.format("%s/%s", openStackId, hostname);
- }
-
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java
deleted file mode 100644
index caef4630544..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-
-/**
- * To avoid duplication of URI construction.
- * This class should be deleted when there's a provision client configured in services xml.
- * @author tonytv
- */
-public class ProvisionEndpoint {
-
- public static final int configServerPort = 19071;
-
- public static URI provisionUri(String configServerHostName, int port) {
- try {
- return new URL("http", configServerHostName, port, "/hack/provision").toURI();
- } catch (URISyntaxException | MalformedURLException e) {
- throw new IllegalArgumentException("Failed creating provisionUri from " + configServerHostName, e);
- }
- }
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java
deleted file mode 100644
index da55ef9a15d..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.container.jaxrs.annotation.Component;
-import com.yahoo.log.LogLevel;
-import com.yahoo.vespa.hosted.provision.Node;
-import com.yahoo.vespa.hosted.provision.Node.State;
-import com.yahoo.vespa.hosted.provision.NodeRepository;
-import com.yahoo.vespa.hosted.provision.node.NodeFlavors;
-import com.yahoo.vespa.hosted.provision.restapi.NodeStateSerializer;
-import com.yahoo.vespa.hosted.provision.restapi.legacy.ContainersForHost.DockerContainer;
-
-import javax.ws.rs.*;
-import javax.ws.rs.core.MediaType;
-import java.util.*;
-import java.util.function.Predicate;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * The provisioning web service used by the provisioning controller to provide nodes to a node repository.
- *
- * @author mortent
- */
-@Path("/provision")
-@Produces(MediaType.APPLICATION_JSON)
-public class ProvisionResource {
-
- private static final Logger log = Logger.getLogger(ProvisionResource.class.getName());
-
- private final NodeRepository nodeRepository;
-
- private final NodeFlavors nodeFlavors;
-
- public ProvisionResource(@Component NodeRepository nodeRepository, @Component NodeFlavors nodeFlavors) {
- super();
- this.nodeRepository = nodeRepository;
- this.nodeFlavors = nodeFlavors;
- }
-
-
- @POST
- @Path("/node")
- @Consumes(MediaType.APPLICATION_JSON)
- public void addNodes(List<HostInfo> hostInfoList) {
- List<Node> nodes = new ArrayList<>();
- for (HostInfo hostInfo : hostInfoList)
- nodes.add(nodeRepository.createNode(hostInfo.openStackId, hostInfo.hostname, Optional.empty(), nodeFlavors.getFlavorOrThrow(hostInfo.flavor), Node.Type.tenant));
- nodeRepository.addNodes(nodes);
- }
-
- @GET
- @Path("/node/required")
- public ProvisionStatus getStatus() {
- ProvisionStatus provisionStatus = new ProvisionStatus();
- provisionStatus.requiredNodes = 0; // This concept has no meaning any more ...
- provisionStatus.decomissionNodes = toHostInfo(nodeRepository.getInactive());
- provisionStatus.failedNodes = toHostInfo(nodeRepository.getFailed());
-
- return provisionStatus;
- }
-
- private List<HostInfo> toHostInfo(List<Node> nodes) {
- List<HostInfo> hostInfoList = new ArrayList<>(nodes.size());
- for (Node node : nodes)
- hostInfoList.add(HostInfo.createHostInfo(node.hostname(), node.openStackId(), "medium"));
- return hostInfoList;
- }
-
-
- @PUT
- @Path("/node/ready")
- public void setReady(String hostName) {
- if ( nodeRepository.getNode(hostName, Node.State.ready).isPresent()) return; // node already 'ready'
-
- Optional<Node> node = nodeRepository.getNode(hostName, Node.State.provisioned, Node.State.dirty);
- if ( ! node.isPresent())
- throw new IllegalArgumentException("Could not set " + hostName + " ready: Not registered as provisioned or dirty");
-
- nodeRepository.setReady(Collections.singletonList(node.get()));
- }
-
- @GET
- @Path("/node/usage/{tenantId}")
- public TenantStatus getTenantUsage(@PathParam("tenantId") String tenantId) {
- TenantStatus ts = new TenantStatus();
- ts.tenantId = tenantId;
- ts.allocated = nodeRepository.getNodeCount(tenantId, Node.State.active);
- ts.reserved = nodeRepository.getNodeCount(tenantId, Node.State.reserved);
-
- Map<String, TenantStatus.ApplicationUsage> appinstanceUsageMap = new HashMap<>();
-
- nodeRepository.getNodes(Node.Type.tenant, Node.State.active).stream()
- .filter(node -> {
- return node.allocation().get().owner().tenant().value().equals(tenantId);
- })
- .forEach(node -> {
- ApplicationId owner = node.allocation().get().owner();
- appinstanceUsageMap.merge(
- String.format("%s:%s", owner.application().value(), owner.instance().value()),
- TenantStatus.ApplicationUsage.create(owner.application().value(), owner.instance().value(), 1),
- (a, b) -> {
- a.usage += b.usage;
- return a;
- }
- );
- });
-
- ts.applications = new ArrayList<>(appinstanceUsageMap.values());
- return ts;
- }
-
- //TODO: move this to nodes/v2/ when the spec for this has been nailed.
- //TODO: Change it to list host nodes, instead of hosts for tenant nodes.
- @GET
- @Path("/dockerhost/{hostname}")
- public ContainersForHost getContainersForHost(@PathParam("hostname") String hostname) {
- List<DockerContainer> dockerContainersForHost =
- nodeRepository.getNodes(Node.Type.tenant, State.active, State.inactive).stream()
- .filter(runsOnDockerHost(hostname))
- .flatMap(ProvisionResource::toDockerContainer)
- .collect(Collectors.toList());
-
- return new ContainersForHost(dockerContainersForHost);
- }
-
- //returns stream since there is no conversion from optional to stream in java.
- private static Stream<DockerContainer> toDockerContainer(Node node) {
- try {
- String dockerImage = node.allocation().get().membership().cluster().dockerImage().orElseThrow(() ->
- new Exception("Docker image not set for node " + node));
-
- return Stream.of(new DockerContainer(
- node.hostname(),
- dockerImage,
- NodeStateSerializer.wireNameOf(node.state()),
- node.allocation().get().restartGeneration().wanted(),
- node.allocation().get().restartGeneration().current()));
- } catch (Exception e) {
- log.log(LogLevel.ERROR, "Ignoring docker container.", e);
- return Stream.empty();
- }
- }
-
- private static Predicate<Node> runsOnDockerHost(String hostname) {
- return node -> node.parentHostname().map(hostname::equals).orElse(false);
- }
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java
deleted file mode 100644
index 7e0eb41627f..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import java.util.List;
-
-/**
- * Value class used to convert to/from JSON.
- *
- * @author mortent
- */
-class ProvisionStatus {
-
- public int requiredNodes;
- public List<HostInfo> decomissionNodes;
- public List<HostInfo> failedNodes;
-
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java
deleted file mode 100644
index 4f20670fa12..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import java.util.List;
-
-/**
- * Value class used to convert to/from JSON.
- *
- * @author Oyvind Gronnesby
- */
-class TenantStatus {
-
- public String tenantId;
- public int allocated;
- public int reserved;
- public List<ApplicationUsage> applications;
-
- public static class ApplicationUsage {
- public String application;
- public String instance;
- public int usage;
-
- public static ApplicationUsage create(String applicationId, String instanceId, int usage) {
- ApplicationUsage appUsage = new ApplicationUsage();
- appUsage.application = applicationId;
- appUsage.instance = instanceId;
- appUsage.usage = usage;
- return appUsage;
- }
- }
-}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java
deleted file mode 100644
index 75ffa3e240e..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.provision.restapi.legacy;
-
-import com.yahoo.osgi.annotation.ExportPackage;
-
-/**
- * Rest API which allows nodes to be added and removed from this node repository
- * This API, aptly named "hack" will be removed once the dependencies are off it - Jon, March 2015
- */ \ No newline at end of file
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java
index da70453c293..1df61f1c6f7 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.restapi.v1;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
@@ -22,7 +23,7 @@ import java.util.Optional;
import java.util.concurrent.Executor;
/**
- * The implementation of the /state/v1 API.
+ * The implementation of the /nodes/v1 API.
* This dumps the content of the node repository on request, possibly with a host filter to return just the single
* matching node.
*
@@ -79,7 +80,7 @@ public class
private void toSlime(Node.State state, Cursor object) {
Cursor nodeArray = null; // create if there are nodes
- for (Node.Type type : Node.Type.values()) {
+ for (NodeType type : NodeType.values()) {
List<Node> nodes = nodeRepository.getNodes(type, state);
for (Node node : nodes) {
if (hostnameFilter.isPresent() && !node.hostname().equals(hostnameFilter.get())) continue;
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java
index 9e240ba6055..1cea59ef79b 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.provision.restapi.v2;
import com.yahoo.config.provision.HostFilter;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
@@ -34,7 +35,7 @@ import java.util.logging.Level;
import static com.yahoo.vespa.config.SlimeUtils.optionalString;
/**
- * The implementation of the /state/v2 API.
+ * The implementation of the /nodes/v2 API.
* See RestApiTest for documentation.
*
* @author bratseth
@@ -198,12 +199,12 @@ public class NodesApiHandler extends LoggingRequestHandler {
nodeTypeFromSlime(inspector.field(nodeTypeKey)));
}
- private Node.Type nodeTypeFromSlime(Inspector object) {
- if (! object.valid()) return Node.Type.tenant; // default
+ private NodeType nodeTypeFromSlime(Inspector object) {
+ if (! object.valid()) return NodeType.tenant; // default
switch (object.asString()) {
- case "tenant" : return Node.Type.tenant;
- case "host" : return Node.Type.host;
- case "proxy" : return Node.Type.proxy;
+ case "tenant" : return NodeType.tenant;
+ case "host" : return NodeType.host;
+ case "proxy" : return NodeType.proxy;
default: throw new IllegalArgumentException("Unknown node type '" + object.asString() + "'");
}
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java
index b81fe0c4417..c245230bfa3 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.restapi.v2;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.slime.Cursor;
@@ -102,7 +103,7 @@ class NodesResponse extends HttpResponse {
/** Outputs the nodes in the given state to a node array */
private void nodesToSlime(Node.State state, Cursor parentObject) {
Cursor nodeArray = parentObject.setArray("nodes");
- for (Node.Type type : Node.Type.values())
+ for (NodeType type : NodeType.values())
toSlime(nodeRepository.getNodes(type, state), nodeArray);
}
@@ -110,7 +111,7 @@ class NodesResponse extends HttpResponse {
private void nodesToSlime(Cursor parentObject) {
Cursor nodeArray = parentObject.setArray("nodes");
for (Node.State state : Node.State.values()) {
- for (Node.Type type : Node.Type.values())
+ for (NodeType type : NodeType.values())
toSlime(nodeRepository.getNodes(type, state), nodeArray);
}
}
@@ -175,7 +176,7 @@ class NodesResponse extends HttpResponse {
toSlime(node.history(), object.setArray("history"));
}
- private String toString(Node.Type type) {
+ private String toString(NodeType type) {
switch(type) {
case tenant: return "tenant";
case host: return "host";
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java
index b312e7c85ca..748a5b6c558 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java
@@ -31,6 +31,19 @@ public class FlavorConfigBuilder {
return flavor;
}
+ public NodeRepositoryConfig.Flavor.Builder addNonStockFlavor(String flavorName, double cpu, double mem, double disk, Flavor.Type type) {
+ NodeRepositoryConfig.Flavor.Builder flavor = new NodeRepositoryConfig.Flavor.Builder();
+ flavor.name(flavorName);
+ flavor.description("Flavor-name-is-" + flavorName);
+ flavor.minDiskAvailableGb(disk);
+ flavor.minCpuCores(cpu);
+ flavor.minMainMemoryAvailableGb(mem);
+ flavor.stock(false);
+ flavor.environment(type.name());
+ builder.flavor(flavor);
+ return flavor;
+ }
+
public void addReplaces(String replaces, NodeRepositoryConfig.Flavor.Builder flavor) {
NodeRepositoryConfig.Flavor.Replaces.Builder flavorReplaces = new NodeRepositoryConfig.Flavor.Replaces.Builder();
flavorReplaces.name(replaces);
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
index ea6f581413e..5e1fd2357cb 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java
@@ -8,6 +8,7 @@ import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostSpec;
import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.Zone;
import com.yahoo.transaction.NestedTransaction;
@@ -47,22 +48,22 @@ public class MockNodeRepository extends NodeRepository {
NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(this, flavors, Zone.defaultZone());
List<Node> nodes = new ArrayList<>();
- nodes.add(createNode("node1", "host1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant));
- nodes.add(createNode("node2", "host2.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant));
- nodes.add(createNode("node3", "host3.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("expensive"), Node.Type.tenant));
+ nodes.add(createNode("node1", "host1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant));
+ nodes.add(createNode("node2", "host2.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant));
+ nodes.add(createNode("node3", "host3.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("expensive"), NodeType.tenant));
// TODO: Use docker flavor
- Node node4 = createNode("node4", "host4.yahoo.com", Optional.of("dockerhost4"), flavors.getFlavorOrThrow("default"), Node.Type.tenant);
+ Node node4 = createNode("node4", "host4.yahoo.com", Optional.of("dockerhost4"), flavors.getFlavorOrThrow("default"), NodeType.tenant);
node4 = node4.with(node4.status().withDockerImage("image-12"));
nodes.add(node4);
- Node node5 = createNode("node5", "host5.yahoo.com", Optional.of("dockerhost"), flavors.getFlavorOrThrow("default"), Node.Type.tenant);
+ Node node5 = createNode("node5", "host5.yahoo.com", Optional.of("dockerhost"), flavors.getFlavorOrThrow("default"), NodeType.tenant);
nodes.add(node5.with(node5.status().withDockerImage("image-123").withVespaVersion(new Version("1.2.3"))));
- nodes.add(createNode("node6", "host6.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant));
- nodes.add(createNode("node7", "host7.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant));
+ nodes.add(createNode("node6", "host6.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant));
+ nodes.add(createNode("node7", "host7.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant));
// 8 and 9 are added by web service calls
- Node node10 = createNode("node10", "host10.yahoo.com", Optional.of("parent.yahoo.com"), flavors.getFlavorOrThrow("default"), Node.Type.tenant);
+ Node node10 = createNode("node10", "host10.yahoo.com", Optional.of("parent.yahoo.com"), flavors.getFlavorOrThrow("default"), NodeType.tenant);
Status node10newStatus = node10.status();
node10newStatus = node10newStatus
.withVespaVersion(Version.fromString("5.104.142"))
@@ -71,8 +72,8 @@ public class MockNodeRepository extends NodeRepository {
node10 = node10.with(node10newStatus);
nodes.add(node10);
- nodes.add(createNode("node55", "host55.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant));
- nodes.add(createNode("parent1", "parent1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.host));
+ nodes.add(createNode("node55", "host55.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant));
+ nodes.add(createNode("parent1", "parent1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.host));
nodes = addNodes(nodes);
nodes.remove(6);
diff --git a/node-repository/src/main/resources/configdefinitions/node-repository.def b/node-repository/src/main/resources/configdefinitions/node-repository.def
index cd053adca61..f9b500594bd 100644
--- a/node-repository/src/main/resources/configdefinitions/node-repository.def
+++ b/node-repository/src/main/resources/configdefinitions/node-repository.def
@@ -18,6 +18,13 @@ flavor[].replaces[].name string
# the expected lifetime of the node (usually three years).
flavor[].cost int default=0
+# A stock flavor is any flavor which we expect to buy more of in the future.
+# Stock flavors are assigned to applications by cost priority.
+#
+# Non-stock flavors are used for nodes for which a fixed amount has already been purchased
+# for some historical reason. These nodes are assigned to applications by exact match and ignoring cost.
+flavor[].stock bool default=true
+
# The type of node (e.g. bare metal, docker..).
flavor[].environment string default="undefined"