diff options
32 files changed, 298 insertions, 155 deletions
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java index 3ea3892e398..4b882023e08 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGeneratorTest.java @@ -64,6 +64,7 @@ public class IdentityDocumentGeneratorTest { Optional.empty(), Optional.empty(), new MockNodeFlavors().getFlavorOrThrow("default"), + Optional.empty(), NodeType.host); Node containerNode = Node.createDockerNode(Set.of("::1"), containerHostname, diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java index a35dfd878c5..2208b470ed8 100644 --- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java +++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java @@ -223,7 +223,14 @@ public class InstanceValidatorTest { MockNodeFlavors flavors = new MockNodeFlavors(); List<Node> nodeList = new ArrayList<>(); for (int i = 0; i < num; i++) { - Node node = Node.create("foo" + i, new IP.Config(Set.of("::1" + i, "::2" + i, "::3" + i), Set.of()), "foo" + i, Optional.empty(), Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant); + Node node = Node.create("foo" + i, + new IP.Config(Set.of("::1" + i, "::2" + i, "::3" + i), Set.of()), + "foo" + i, + Optional.empty(), + Optional.empty(), + flavors.getFlavorOrThrow("default"), + Optional.empty(), + NodeType.tenant); nodeList.add(node); } return nodeList; 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 4ff0fee2eb7..3778b7aa625 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 @@ -7,6 +7,7 @@ import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.TenantName; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.Generation; @@ -41,6 +42,7 @@ public final class Node { private final NodeType type; private final Reports reports; private final Optional<String> modelName; + private final Optional<TenantName> reservedTo; /** Record of the last event of each type happening to this node */ private final History history; @@ -50,52 +52,46 @@ public final class Node { /** Creates a node in the initial state (reserved) */ public static Node createDockerNode(Set<String> ipAddresses, String hostname, String parentHostname, NodeResources resources, NodeType type) { - return new Node("fake-" + hostname, new IP.Config(ipAddresses, Set.of()), hostname, Optional.of(parentHostname), new Flavor(resources), Status.initial(), State.reserved, - Optional.empty(), History.empty(), type, new Reports(), Optional.empty()); + return new Node("fake-" + hostname, new IP.Config(ipAddresses, Set.of()), hostname, Optional.of(parentHostname), + new Flavor(resources), Status.initial(), State.reserved, + Optional.empty(), History.empty(), type, new Reports(), Optional.empty(), Optional.empty()); } /** Creates a node in the initial state (provisioned) */ - public static Node create(String openStackId, IP.Config ipConfig, String hostname, Optional<String> parentHostname, Optional<String> modelName, Flavor flavor, NodeType type) { + public static Node create(String openStackId, IP.Config ipConfig, String hostname, Optional<String> parentHostname, + Optional<String> modelName, Flavor flavor, Optional<TenantName> reservedTo, NodeType type) { return new Node(openStackId, ipConfig, hostname, parentHostname, flavor, Status.initial(), State.provisioned, - Optional.empty(), History.empty(), type, new Reports(), modelName); + Optional.empty(), History.empty(), type, new Reports(), modelName, reservedTo); } /** Creates a node. See also the {@code create} helper methods. */ public Node(String id, IP.Config ipConfig, String hostname, Optional<String> parentHostname, Flavor flavor, Status status, State state, Optional<Allocation> allocation, History history, NodeType type, - Reports reports, Optional<String> modelName) { - Objects.requireNonNull(id, "A node must have an ID"); - requireNonEmptyString(hostname, "A node must have a hostname"); - Objects.requireNonNull(ipConfig, "A node must a have an IP config"); - requireNonEmptyString(parentHostname, "A parent host name must be a proper value"); - Objects.requireNonNull(flavor, "A node must have a flavor"); - Objects.requireNonNull(status, "A node must have a status"); - Objects.requireNonNull(state, "A null node state is not permitted"); - Objects.requireNonNull(allocation, "A null node allocation is not permitted"); - Objects.requireNonNull(history, "A null node history is not permitted"); - Objects.requireNonNull(type, "A null node type is not permitted"); - Objects.requireNonNull(reports, "A null reports is not permitted"); - Objects.requireNonNull(modelName, "A null modelName is not permitted"); + Reports reports, Optional<String> modelName, Optional<TenantName> reservedTo) { + this.id = Objects.requireNonNull(id, "A node must have an ID"); + this.hostname = requireNonEmptyString(hostname, "A node must have a hostname"); + this.ipConfig = Objects.requireNonNull(ipConfig, "A node must a have an IP config"); + this.parentHostname = requireNonEmptyString(parentHostname, "A parent host name must be a proper value"); + this.flavor = Objects.requireNonNull(flavor, "A node must have a flavor"); + this.status = Objects.requireNonNull(status, "A node must have a status"); + this.state = Objects.requireNonNull(state, "A null node state is not permitted"); + this.allocation = Objects.requireNonNull(allocation, "A null node allocation is not permitted"); + this.history = Objects.requireNonNull(history, "A null node history is not permitted"); + this.type = Objects.requireNonNull(type, "A null node type is not permitted"); + this.reports = Objects.requireNonNull(reports, "A null reports is not permitted"); + this.modelName = Objects.requireNonNull(modelName, "A null modelName is not permitted"); + this.reservedTo = Objects.requireNonNull(reservedTo, "reservedTo cannot be null"); if (state == State.active) requireNonEmpty(ipConfig.primary(), "An active node must have at least one valid IP address"); + if (parentHostname.isPresent()) { if (!ipConfig.pool().asSet().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"); } - this.hostname = hostname; - this.ipConfig = ipConfig; - this.parentHostname = parentHostname; - this.id = id; - this.flavor = flavor; - this.status = status; - this.state = state; - this.allocation = allocation; - this.history = history; - this.type = type; - this.reports = reports; - this.modelName = modelName; + if (type != NodeType.host && reservedTo.isPresent()) + throw new IllegalArgumentException("Only hosts can be reserved to a tenant"); } /** Returns the IP addresses of this node */ @@ -166,6 +162,12 @@ public final class Node { public Optional<String> modelName() { return modelName; } /** + * Returns the tenant this node is reserved to, if any. Only hosts can be reserved to a tenant. + * If this is set, resources on this host cannot be allocated to any other tenant + */ + public Optional<TenantName> reservedTo() { return reservedTo; } + + /** * Returns a copy of this node with wantToRetire set to the given value and updated history. * If given wantToRetire is equal to the current, the method is no-op. */ @@ -209,37 +211,42 @@ public final class Node { /** Returns a node with the status assigned to the given value */ public Node with(Status status) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName, reservedTo); } /** Returns a node with the type assigned to the given value */ public Node with(NodeType type) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName, reservedTo); } /** Returns a node with the flavor assigned to the given value */ public Node with(Flavor flavor) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo); } /** Returns a copy of this with the reboot generation set to generation */ public Node withReboot(Generation generation) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status.withReboot(generation), state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status.withReboot(generation), state, + allocation, history, type, reports, modelName, reservedTo); } /** Returns a copy of this with the openStackId set */ public Node withOpenStackId(String openStackId) { - return new Node(openStackId, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(openStackId, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo); } /** Returns a copy of this with model name set to given value */ public Node withModelName(String modelName) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, Optional.of(modelName)); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, Optional.of(modelName), reservedTo); } /** Returns a copy of this with model name cleared */ public Node withoutModelName() { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, Optional.empty()); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, Optional.empty(), reservedTo); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -265,26 +272,32 @@ public final class Node { */ public Node with(Allocation allocation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.of(allocation), history, type, reports, modelName); + Optional.of(allocation), history, type, reports, modelName, reservedTo); } /** Returns a new Node without an allocation. */ public Node withoutAllocation() { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.empty(), history, type, reports, modelName); + Optional.empty(), history, type, reports, modelName, reservedTo); } /** Returns a copy of this node with IP config set to the given value. */ public Node with(IP.Config ipConfig) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName); + allocation, history, type, reports, modelName, reservedTo); } /** Returns a copy of this node with the parent hostname assigned to the given value. */ public Node withParentHostname(String parentHostname) { return new Node(id, ipConfig, hostname, Optional.of(parentHostname), flavor, status, state, - allocation, history, type, reports, modelName); + allocation, history, type, reports, modelName, reservedTo); + } + + /** Returns a copy of this node which is not reserved to a tenant */ + public Node withoutReservedTo() { + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, Optional.empty()); } /** Returns a copy of this node with the current reboot generation set to the given number at the given instant */ @@ -316,28 +329,32 @@ public final class Node { /** Returns a copy of this node with the given history. */ public Node with(History history) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo); } public Node with(Reports reports) { - return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, history, type, reports, modelName); + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo); } - private static void requireNonEmptyString(Optional<String> value, String message) { + private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { Objects.requireNonNull(value, message); value.ifPresent(v -> requireNonEmptyString(v, message)); + return value; } - private static void requireNonEmptyString(String value, String message) { + private static String requireNonEmptyString(String value, String message) { Objects.requireNonNull(value, message); if (value.trim().isEmpty()) throw new IllegalArgumentException(message + ", but was '" + value + "'"); + return value; } - private static void requireNonEmpty(Set<String> values, String message) { - if (values == null || values.isEmpty()) { + private static Set<String> requireNonEmpty(Set<String> values, String message) { + if (values == null || values.isEmpty()) throw new IllegalArgumentException(message); - } + return values; } /** Computes the allocation skew of a host node */ 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 1c9e1445447..d5aaf5dbad7 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 @@ -10,6 +10,7 @@ import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provisioning.NodeRepositoryConfig; import com.yahoo.transaction.Mutex; @@ -309,16 +310,15 @@ public class NodeRepository extends AbstractComponent { /** Creates a new node object, without adding it to the node repo. If no IP address is given, it will be resolved */ public Node createNode(String openStackId, String hostname, IP.Config ipConfig, Optional<String> parentHostname, - Flavor flavor, NodeType type) { + Flavor flavor, Optional<TenantName> reservedTo, NodeType type) { if (ipConfig.primary().isEmpty()) { // TODO: Remove this. Only test code hits this path ipConfig = ipConfig.with(nameResolver.getAllByNameOrThrow(hostname)); } - return Node.create(openStackId, ipConfig, hostname, parentHostname, Optional.empty(), flavor, type); + return Node.create(openStackId, ipConfig, hostname, parentHostname, Optional.empty(), flavor, reservedTo, type); } - public Node createNode(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, - NodeType type) { - return createNode(openStackId, hostname, IP.Config.EMPTY, parentHostname, flavor, type); + public Node createNode(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, NodeType type) { + return createNode(openStackId, hostname, IP.Config.EMPTY, parentHostname, flavor, Optional.empty(), type); } /** Adds a list of newly created docker container nodes to the node repository as <i>reserved</i> nodes */ @@ -341,7 +341,7 @@ public class NodeRepository extends AbstractComponent { /** Adds a list of (newly created) nodes to the node repository as <i>provisioned</i> nodes */ public List<Node> addNodes(List<Node> nodes) { - try (Mutex lock = lockAllocation()) { + try (Mutex lock = lockUnallocated()) { for (int i = 0; i < nodes.size(); i++) { var node = nodes.get(i); var message = "Cannot add " + node.hostname() + ": A node with this name already exists"; @@ -361,7 +361,7 @@ public class NodeRepository extends AbstractComponent { /** Sets a list of nodes ready and returns the nodes in the ready state */ public List<Node> setReady(List<Node> nodes, Agent agent, String reason) { - try (Mutex lock = lockAllocation()) { + try (Mutex lock = lockUnallocated()) { List<Node> nodesWithResetFields = nodes.stream() .map(node -> { if (node.state() != Node.State.provisioned && node.state() != Node.State.dirty) @@ -585,7 +585,7 @@ public class NodeRepository extends AbstractComponent { } public List<Node> removeRecursively(Node node, boolean force) { - try (Mutex lock = lockAllocation()) { + try (Mutex lock = lockUnallocated()) { List<Node> removed = new ArrayList<>(); if (node.type().isDockerHost()) { @@ -713,7 +713,7 @@ public class NodeRepository extends AbstractComponent { // perform operation while holding locks List<Node> resultingNodes = new ArrayList<>(); - try (Mutex lock = lockAllocation()) { + try (Mutex lock = lockUnallocated()) { for (Node node : unallocatedNodes) resultingNodes.add(action.apply(node, lock)); } @@ -738,12 +738,12 @@ public class NodeRepository extends AbstractComponent { /** Create a lock with a timeout which provides exclusive rights to making changes to the given application */ public Mutex lock(ApplicationId application, Duration timeout) { return db.lock(application, timeout); } - /** Create a lock which provides exclusive rights to allocating nodes */ - public Mutex lockAllocation() { return db.lockInactive(); } + /** Create a lock which provides exclusive rights to modifying unallocated nodes */ + public Mutex lockUnallocated() { return db.lockInactive(); } /** Acquires the appropriate lock for this node */ public Mutex lock(Node node) { - return node.allocation().isPresent() ? lock(node.allocation().get().owner()) : lockAllocation(); + return node.allocation().isPresent() ? lock(node.allocation().get().owner()) : lockUnallocated(); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java index 1c27f9d1713..bb6d53d3304 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java @@ -59,7 +59,7 @@ public class DynamicProvisioningMaintainer extends Maintainer { protected void maintain() { if (! dynamicProvisioningEnabled.value()) return; - try (Mutex lock = nodeRepository().lockAllocation()) { + try (Mutex lock = nodeRepository().lockUnallocated()) { NodeList nodes = nodeRepository().list(); updateProvisioningNodes(nodes, lock); 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 dca5c092ad2..424d6409670 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 @@ -97,7 +97,7 @@ public class NodeFailer extends Maintainer { int throttledNodeFailures = 0; // Ready nodes - try (Mutex lock = nodeRepository().lockAllocation()) { + try (Mutex lock = nodeRepository().lockUnallocated()) { updateNodeLivenessEventsForReadyNodes(lock); for (Map.Entry<Node, String> entry : getReadyNodesByFailureReason().entrySet()) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index a28845109dc..ce0bcc2e337 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -206,7 +206,7 @@ public class CuratorDatabaseClient { toState, toState.isAllocated() ? node.allocation() : Optional.empty(), node.history().recordStateTransition(node.state(), toState, agent, clock.instant()), - node.type(), node.reports(), node.modelName()); + node.type(), node.reports(), node.modelName(), node.reservedTo()); writeNode(toState, curatorTransaction, node, newNode); writtenNodes.add(newNode); } 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 2cbfbc349a6..728ec344c3f 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 @@ -19,6 +19,7 @@ import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; +import com.yahoo.slime.Type; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.node.Agent; @@ -77,6 +78,7 @@ public class NodeSerializer { private static final String firmwareCheckKey = "firmwareCheck"; private static final String reportsKey = "reports"; private static final String modelNameKey = "modelName"; + private static final String reservedToKey = "reservedTo"; // Node resource fields // ...for hosts and nodes allocated by legacy flavor specs @@ -149,6 +151,7 @@ public class NodeSerializer { node.status().firmwareVerifiedAt().ifPresent(instant -> object.setLong(firmwareCheckKey, instant.toEpochMilli())); node.reports().toSlime(object, reportsKey); node.modelName().ifPresent(modelName -> object.setString(modelNameKey, modelName)); + node.reservedTo().ifPresent(tenant -> object.setString(reservedToKey, tenant.value())); } private void toSlime(Flavor flavor, Cursor object) { @@ -222,7 +225,8 @@ public class NodeSerializer { historyFromSlime(object.field(historyKey)), nodeTypeFromString(object.field(nodeTypeKey).asString()), Reports.fromSlime(object.field(reportsKey)), - modelNameFromSlime(object)); + modelNameFromSlime(object), + reservedToFromSlime(object.field(reservedToKey))); } private Status statusFromSlime(Inspector object) { @@ -341,6 +345,13 @@ public class NodeSerializer { return Optional.empty(); } + private Optional<TenantName> reservedToFromSlime(Inspector object) { + if (! object.valid()) return Optional.empty(); + if (object.type() != Type.STRING) + throw new IllegalArgumentException("Expected 'reservedTo' to be a string but is " + object); + return Optional.of(TenantName.from(object.asString())); + } + // ----------------- Enum <-> string mappings ---------------------------------------- /** Returns the event type, or null if this event type should be ignored */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java index a7519de3776..36034b62cfb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Activator.java @@ -6,7 +6,6 @@ import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.ParentHostUnavailableException; -import com.yahoo.log.LogLevel; import com.yahoo.transaction.Mutex; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.hosted.provision.Node; @@ -90,6 +89,22 @@ class Activator { nodeRepository.deactivate(activeToRemove, transaction); nodeRepository.activate(updateFrom(hosts, continuedActive), transaction); // update active with any changes nodeRepository.activate(updatePortsFrom(hosts, reservedToActivate), transaction); + unreserveParentsOf(reservedToActivate); + } + + /** When a tenant node is activated on a host, we can open up that host for use by others */ + private void unreserveParentsOf(List<Node> nodes) { + for (Node node : nodes) { + if ( node.parentHostname().isEmpty()) continue; + Optional<Node> parent = nodeRepository.getNode(node.parentHostname().get()); + if (parent.isEmpty()) continue; + if (parent.get().reservedTo().isEmpty()) continue; + try (Mutex lock = nodeRepository.lock(parent.get())) { + Optional<Node> lockedParent = nodeRepository.getNode(parent.get().hostname()); + if (lockedParent.isEmpty()) continue; + nodeRepository.write(lockedParent.get().withoutReservedTo(), lock); + } + } } /** Activate load balancers */ @@ -117,9 +132,9 @@ class Activator { .filter(node -> node.state() != Node.State.active) .map(Node::hostname) .collect(Collectors.toSet()); - long numNonActive = nonActiveHosts.size(); - if (numNonActive > 0) { - long numActive = parentHostnames.size() - numNonActive; + + if (nonActiveHosts.size() > 0) { + long numActive = parentHostnames.size() - nonActiveHosts.size(); var messageBuilder = new StringBuilder() .append(numActive).append("/").append(parentHostnames.size()) .append(" hosts for ") 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 9cc4b3f3bdb..6686160982a 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 @@ -73,7 +73,7 @@ public class GroupPreparer { try (Mutex lock = nodeRepository.lock(application)) { // Lock ready pool to ensure that the same nodes are not simultaneously allocated by others - try (Mutex allocationLock = nodeRepository.lockAllocation()) { + try (Mutex allocationLock = nodeRepository.lockUnallocated()) { // Create a prioritized set of nodes LockedNodeList nodeList = nodeRepository.list(allocationLock); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java index ef6e4747f94..ebd6a01e61f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeAllocation.java @@ -140,9 +140,9 @@ class NodeAllocation { continue; } node.node = offered.allocate(application, - ClusterMembership.from(cluster, highestIndex.add(1)), - requestedNodes.resources().orElse(node.node.flavor().resources()), - clock.instant()); + ClusterMembership.from(cluster, highestIndex.add(1)), + requestedNodes.resources().orElse(node.node.flavor().resources()), + clock.instant()); accepted.add(acceptNode(node, false, false)); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java index a17d92117fb..a5fd37640d8 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java @@ -40,7 +40,7 @@ public class NodePrioritizer { private final LockedNodeList allNodes; private final DockerHostCapacity capacity; private final NodeSpec requestedNodes; - private final ApplicationId appId; + private final ApplicationId application; private final ClusterSpec clusterSpec; private final NameResolver nameResolver; private final boolean isDocker; @@ -50,19 +50,19 @@ public class NodePrioritizer { private final int currentClusterSize; private final Set<Node> spareHosts; - NodePrioritizer(LockedNodeList allNodes, ApplicationId appId, ClusterSpec clusterSpec, NodeSpec nodeSpec, + NodePrioritizer(LockedNodeList allNodes, ApplicationId application, ClusterSpec clusterSpec, NodeSpec nodeSpec, int spares, int wantedGroups, NameResolver nameResolver, HostResourcesCalculator hostResourcesCalculator, boolean inPlaceResizeEnabled) { this.allNodes = allNodes; this.capacity = new DockerHostCapacity(allNodes, hostResourcesCalculator); this.requestedNodes = nodeSpec; this.clusterSpec = clusterSpec; - this.appId = appId; + this.application = application; this.nameResolver = nameResolver; this.spareHosts = findSpareHosts(allNodes, capacity, spares); this.inPlaceResizeEnabled = inPlaceResizeEnabled; - NodeList nodesInCluster = allNodes.owner(appId).type(clusterSpec.type()).cluster(clusterSpec.id()); + NodeList nodesInCluster = allNodes.owner(application).type(clusterSpec.type()).cluster(clusterSpec.id()); NodeList nonRetiredNodesInCluster = nodesInCluster.not().retired(); long currentGroups = nonRetiredNodesInCluster.state(Node.State.active).stream() .flatMap(node -> node.allocation() @@ -127,13 +127,14 @@ public class NodePrioritizer { if ( ! isDocker) return; LockedNodeList candidates = allNodes - .filter(node -> node.type() != NodeType.host || ALLOCATABLE_HOST_STATES.contains(node.state())); + .filter(node -> node.type() != NodeType.host || ALLOCATABLE_HOST_STATES.contains(node.state())) + .filter(node -> node.reservedTo().isEmpty() || node.reservedTo().get().equals(application.tenant())); if (exclusively) { Set<String> candidateHostnames = candidates.asList().stream() .filter(node -> node.type() == NodeType.tenant) .filter(node -> node.allocation() - .map(a -> a.owner().tenant().equals(appId.tenant())) + .map(a -> a.owner().tenant().equals(this.application.tenant())) .orElse(false)) .flatMap(node -> node.parentHostname().stream()) .collect(Collectors.toSet()); @@ -152,7 +153,7 @@ public class NodePrioritizer { if (host.status().wantToRetire()) continue; boolean hostHasCapacityForWantedFlavor = capacity.hasCapacity(host, wantedResources); - boolean conflictingCluster = allNodes.childrenOf(host).owner(appId).asList().stream() + boolean conflictingCluster = allNodes.childrenOf(host).owner(application).asList().stream() .anyMatch(child -> child.allocation().get().membership().cluster().id().equals(clusterSpec.id())); if (!hostHasCapacityForWantedFlavor || conflictingCluster) continue; @@ -188,7 +189,7 @@ public class NodePrioritizer { .filter(node -> node.type() == requestedNodes.type()) .filter(node -> legalStates.contains(node.state())) .filter(node -> node.allocation().isPresent()) - .filter(node -> node.allocation().get().owner().equals(appId)) + .filter(node -> node.allocation().get().owner().equals(application)) .map(node -> toPrioritizable(node, false, false)) .forEach(prioritizableNode -> nodes.put(prioritizableNode.node, prioritizableNode)); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNode.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNode.java index c7732695069..6183bffe5ba 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNode.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNode.java @@ -87,6 +87,10 @@ class PrioritizableNode implements Comparable<PrioritizableNode> { throw new IllegalStateException("Nodes " + this.node + " and " + other.node + " have different states"); if (this.parent.isPresent() && other.parent.isPresent()) { + // Prefer reserved hosts (that they are reserved to the right tenant is ensured elsewhere) + if ( this.parent.get().reservedTo().isPresent() && ! other.parent.get().reservedTo().isPresent()) return -1; + if ( ! this.parent.get().reservedTo().isPresent() && other.parent.get().reservedTo().isPresent()) return 1; + int diskCostDifference = NodeResources.DiskSpeed.compare(this.parent.get().flavor().resources().diskSpeed(), other.parent.get().flavor().resources().diskSpeed()); if (diskCostDifference != 0) diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java index 451ab089a8d..b979ccda740 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisionedHost.java @@ -34,7 +34,7 @@ public class ProvisionedHost { /** Generate {@link Node} instance representing the provisioned physical host */ public Node generateHost() { - return Node.create(id, IP.Config.EMPTY, hostHostname, Optional.empty(), Optional.empty(), hostFlavor, NodeType.host); + return Node.create(id, IP.Config.EMPTY, hostHostname, Optional.empty(), Optional.empty(), hostFlavor, Optional.empty(), NodeType.host); } /** Generate {@link Node} instance representing the node running on this physical host */ 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 692402ea914..c64d1a9b5ff 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 @@ -8,6 +8,7 @@ import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; @@ -19,6 +20,7 @@ import com.yahoo.restapi.ResourceResponse; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; +import com.yahoo.slime.Type; import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.provision.NoSuchNodeException; import com.yahoo.vespa.hosted.provision.Node; @@ -225,14 +227,15 @@ public class NodesApiHandler extends LoggingRequestHandler { Set<String> ipAddressPool = new HashSet<>(); inspector.field("additionalIpAddresses").traverse((ArrayTraverser) (i, item) -> ipAddressPool.add(item.asString())); - return Node.create( - inspector.field("openStackId").asString(), - new IP.Config(ipAddresses, ipAddressPool), - inspector.field("hostname").asString(), - parentHostname, - modelName, - flavorFromSlime(inspector), - nodeTypeFromSlime(inspector.field("type"))); + return Node.create(inspector.field("openStackId").asString(), + new IP.Config(ipAddresses, ipAddressPool), + inspector.field("hostname").asString(), + parentHostname, + modelName, + flavorFromSlime(inspector), + reservedToFromSlime(inspector.field("reservedTo")), + nodeTypeFromSlime(inspector.field("type")) + ); } private Flavor flavorFromSlime(Inspector inspector) { @@ -277,6 +280,13 @@ public class NodesApiHandler extends LoggingRequestHandler { return serializer.typeFrom(object.asString()); } + private Optional<TenantName> reservedToFromSlime(Inspector object) { + if (! object.valid()) return Optional.empty(); + if ( object.type() != Type.STRING) + throw new IllegalArgumentException("Expected 'reservedTo' to be a string but is " + object); + return Optional.of(TenantName.from(object.asString())); + } + public static NodeFilter toNodeFilter(HttpRequest request) { NodeFilter filter = NodeHostFilter.from(HostFilter.from(request.getProperty("hostname"), request.getProperty("flavor"), 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 7ed05432e01..0410bbd8064 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 @@ -146,6 +146,7 @@ class NodesResponse extends HttpResponse { } object.setString("openStackId", node.id()); object.setString("flavor", node.flavor().name()); + node.reservedTo().ifPresent(reservedTo -> object.setString("reservedTo", reservedTo.value())); if (node.flavor().isConfigured()) object.setDouble("cpuCores", node.flavor().getMinCpuCores()); toSlime(node.flavor().resources(), object.setObject("resources")); 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 0d331ac0f3f..1817470a63b 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 @@ -70,34 +70,34 @@ public class MockNodeRepository extends NodeRepository { // Regular nodes nodes.add(createNode("node1", "host1.yahoo.com", ipConfig(1), Optional.empty(), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant)); nodes.add(createNode("node2", "host2.yahoo.com", ipConfig(2), Optional.empty(), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant)); nodes.add(createNode("node3", "host3.yahoo.com", ipConfig(3), Optional.empty(), - new Flavor(new NodeResources(0.5, 48, 500, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(0.5, 48, 500, 1, fast, local)), Optional.empty(), NodeType.tenant)); Node node4 = createNode("node4", "host4.yahoo.com", ipConfig(4), Optional.of("dockerhost1.yahoo.com"), - new Flavor(new NodeResources(1, 4, 100, 1, fast, local)), NodeType.tenant); + new Flavor(new NodeResources(1, 4, 100, 1, fast, local)), Optional.empty(), NodeType.tenant); node4 = node4.with(node4.status() .withVespaVersion(new Version("6.41.0")) .withDockerImage(DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa:6.41.0"))); nodes.add(node4); Node node5 = createNode("node5", "host5.yahoo.com", ipConfig(5), Optional.of("dockerhost2.yahoo.com"), - new Flavor(new NodeResources(1, 8, 100, 1, slow, remote)), NodeType.tenant); + new Flavor(new NodeResources(1, 8, 100, 1, slow, remote)), Optional.empty(), NodeType.tenant); nodes.add(node5.with(node5.status() .withVespaVersion(new Version("1.2.3")) .withDockerImage(DockerImage.fromString("docker-registry.domain.tld:8080/dist/vespa:1.2.3")))); nodes.add(createNode("node6", "host6.yahoo.com", ipConfig(6), Optional.empty(), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant)); Node node7 = createNode("node7", "host7.yahoo.com", ipConfig(7), Optional.empty(), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant); nodes.add(node7); // 8, 9, 11 and 12 are added by web service calls Node node10 = createNode("node10", "host10.yahoo.com", ipConfig(10), Optional.of("parent1.yahoo.com"), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant); Status node10newStatus = node10.status(); node10newStatus = node10newStatus .withVespaVersion(Version.fromString("5.104.142")) @@ -106,26 +106,26 @@ public class MockNodeRepository extends NodeRepository { nodes.add(node10); Node node55 = createNode("node55", "host55.yahoo.com", ipConfig(55), Optional.empty(), - new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), NodeType.tenant); + new Flavor(new NodeResources(2, 8, 50, 1, fast, local)), Optional.empty(), NodeType.tenant); nodes.add(node55.with(node55.status().withWantToRetire(true).withWantToDeprovision(true))); /* Setup docker hosts (two of these will be reserved for spares */ nodes.add(createNode("dockerhost1", "dockerhost1.yahoo.com", ipConfig(100, 1, 3), Optional.empty(), - flavors.getFlavorOrThrow("large"), NodeType.host)); + flavors.getFlavorOrThrow("large"), Optional.empty(), NodeType.host)); nodes.add(createNode("dockerhost2", "dockerhost2.yahoo.com", ipConfig(101, 1, 3), Optional.empty(), - flavors.getFlavorOrThrow("large"), NodeType.host)); + flavors.getFlavorOrThrow("large"), Optional.empty(), NodeType.host)); nodes.add(createNode("dockerhost3", "dockerhost3.yahoo.com", ipConfig(102, 1, 3), Optional.empty(), - flavors.getFlavorOrThrow("large"), NodeType.host)); + flavors.getFlavorOrThrow("large"), Optional.empty(), NodeType.host)); nodes.add(createNode("dockerhost4", "dockerhost4.yahoo.com", ipConfig(103, 1, 3), Optional.empty(), - flavors.getFlavorOrThrow("large"), NodeType.host)); + flavors.getFlavorOrThrow("large"), Optional.empty(), NodeType.host)); nodes.add(createNode("dockerhost5", "dockerhost5.yahoo.com", ipConfig(104, 1, 3), Optional.empty(), - flavors.getFlavorOrThrow("large"), NodeType.host)); + flavors.getFlavorOrThrow("large"), Optional.empty(), NodeType.host)); // Config servers nodes.add(createNode("cfg1", "cfg1.yahoo.com", ipConfig(201), Optional.empty(), - flavors.getFlavorOrThrow("default"), NodeType.config)); + flavors.getFlavorOrThrow("default"), Optional.empty(), NodeType.config)); nodes.add(createNode("cfg2", "cfg2.yahoo.com", ipConfig(202), Optional.empty(), - flavors.getFlavorOrThrow("default"), NodeType.config)); + flavors.getFlavorOrThrow("default"), Optional.empty(), NodeType.config)); // Ready all nodes, except 7 and 55 nodes = addNodes(nodes); @@ -167,9 +167,9 @@ public class MockNodeRepository extends NodeRepository { List<Node> largeNodes = new ArrayList<>(); largeNodes.add(createNode("node13", "host13.yahoo.com", ipConfig(13), Optional.empty(), - new Flavor(new NodeResources(10, 48, 500, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(10, 48, 500, 1, fast, local)), Optional.empty(), NodeType.tenant)); largeNodes.add(createNode("node14", "host14.yahoo.com", ipConfig(14), Optional.empty(), - new Flavor(new NodeResources(10, 48, 500, 1, fast, local)), NodeType.tenant)); + new Flavor(new NodeResources(10, 48, 500, 1, fast, local)), Optional.empty(), NodeType.tenant)); addNodes(largeNodes); setReady(largeNodes, Agent.system, getClass().getSimpleName()); ApplicationId app4 = ApplicationId.from(TenantName.from("tenant4"), ApplicationName.from("application4"), InstanceName.from("instance4")); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/CapacityCheckerTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/CapacityCheckerTester.java index da174a8d38c..afac44856c9 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/CapacityCheckerTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/CapacityCheckerTester.java @@ -133,8 +133,8 @@ public class CapacityCheckerTester { NodeResources nr = containingNodeResources(childResources, excessCapacity); Node node = nodeRepository.createNode(hostname, hostname, - new IP.Config(Set.of("::"), availableIps), Optional.empty(), - new Flavor(nr), NodeType.host); + new IP.Config(Set.of("::"), availableIps), Optional.empty(), + new Flavor(nr), Optional.empty(), NodeType.host); hosts.add(node); } return hosts; @@ -152,8 +152,8 @@ public class CapacityCheckerTester { .mapToObj(n -> String.format("%04X::%04X", hostid, n)) .collect(Collectors.toSet()); Node node = nodeRepository.createNode(hostname, hostname, - new IP.Config(Set.of("::"), availableIps), Optional.empty(), - new Flavor(capacity), NodeType.host); + new IP.Config(Set.of("::"), availableIps), Optional.empty(), + new Flavor(capacity), Optional.empty(), NodeType.host); hosts.add(node); } return hosts; @@ -263,8 +263,8 @@ public class CapacityCheckerTester { Flavor f = new Flavor(nr); Node node = nodeRepository.createNode(nodeModel.id, nodeModel.hostname, - new IP.Config(nodeModel.ipAddresses, nodeModel.additionalIpAddresses), - nodeModel.parentHostname, f, nodeModel.type); + new IP.Config(nodeModel.ipAddresses, nodeModel.additionalIpAddresses), + nodeModel.parentHostname, f, Optional.empty(), nodeModel.type); if (membership != null) { return node.allocate(owner, membership, node.flavor().resources(), Instant.now()); 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 71788fb1a30..c7a1486a1a4 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 @@ -204,7 +204,7 @@ public class DynamicProvisioningMaintainerTest { false)); var ipConfig = new IP.Config(state == Node.State.active ? Set.of("::1") : Set.of(), Set.of()); return new Node("fake-id-" + hostname, ipConfig, hostname, parentHostname, flavor, Status.initial(), - state, allocation, History.empty(), nodeType, new Reports(), Optional.empty()); + state, allocation, History.empty(), nodeType, new Reports(), Optional.empty(), Optional.empty()); } } }
\ No newline at end of file diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java index 539d8c7cff2..2a8783748ef 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java @@ -119,7 +119,7 @@ public class MetricsReporterTest { Set<String> ipAddressPool = Set.of("::2", "::3", "::4", "::5"); Node dockerHost = Node.create("openStackId1", new IP.Config(Set.of("::1"), ipAddressPool), "dockerHost", - Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), NodeType.host); + Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), Optional.empty(), NodeType.host); nodeRepository.addNodes(List.of(dockerHost)); nodeRepository.dirtyRecursively("dockerHost", Agent.system, getClass().getSimpleName()); nodeRepository.setReady("dockerHost", Agent.system, getClass().getSimpleName()); @@ -127,12 +127,12 @@ public class MetricsReporterTest { Node container1 = Node.createDockerNode(Set.of("::2"), "container1", "dockerHost", new NodeResources(1, 3, 2, 1), NodeType.tenant); container1 = container1.with(allocation(Optional.of("app1"), container1).get()); - nodeRepository.addDockerNodes(new LockedNodeList(List.of(container1), nodeRepository.lockAllocation())); + nodeRepository.addDockerNodes(new LockedNodeList(List.of(container1), nodeRepository.lockUnallocated())); Node container2 = Node.createDockerNode(Set.of("::3"), "container2", "dockerHost", new NodeResources(2, 4, 4, 1), NodeType.tenant); container2 = container2.with(allocation(Optional.of("app2"), container2).get()); - nodeRepository.addDockerNodes(new LockedNodeList(List.of(container2), nodeRepository.lockAllocation())); + nodeRepository.addDockerNodes(new LockedNodeList(List.of(container2), nodeRepository.lockUnallocated())); Orchestrator orchestrator = mock(Orchestrator.class); ServiceMonitor serviceMonitor = mock(ServiceMonitor.class); 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 5e44b2e903e..fcf2ab5a52d 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 @@ -179,7 +179,7 @@ public class IPTest { private static Node createNode(Set<String> ipAddresses) { return Node.create("id1", new IP.Config(Set.of("127.0.0.1"), ipAddresses), "host1", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("default"), - NodeType.host); + Optional.empty(), NodeType.host); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java index 08e7772b5ba..8e048001b98 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/SerializationTest.java @@ -229,7 +229,7 @@ public class SerializationTest { @Test public void serialize_parentHostname() { final String parentHostname = "parent.yahoo.com"; - Node node = Node.create("myId", new IP.Config(Set.of("127.0.0.1"), Set.of()), "myHostname", Optional.of(parentHostname), Optional.empty(), nodeFlavors.getFlavorOrThrow("default"), NodeType.tenant); + Node node = Node.create("myId", new IP.Config(Set.of("127.0.0.1"), Set.of()), "myHostname", Optional.of(parentHostname), Optional.empty(), nodeFlavors.getFlavorOrThrow("default"), Optional.empty(), NodeType.tenant); Node deserializedNode = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(node)); assertEquals(parentHostname, deserializedNode.parentHostname().get()); @@ -441,7 +441,8 @@ public class SerializationTest { Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("default"), - NodeType.tenant); + Optional.empty(), NodeType.tenant + ); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/AllocationSimulator.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/AllocationSimulator.java index 7e7338ab9ae..2c01cdde932 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/AllocationSimulator.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/AllocationSimulator.java @@ -81,7 +81,7 @@ public class AllocationSimulator { var ipConfig = new IP.Config(Set.of("127.0.0.1"), parent.isPresent() ? Set.of() : getAdditionalIP()); return new Node("fake", ipConfig, hostname, parent, flavor, Status.initial(), parent.isPresent() ? Node.State.ready : Node.State.active, allocation(tenant, flavor), History.empty(), - parent.isPresent() ? NodeType.tenant : NodeType.host, new Reports(), Optional.empty()); + parent.isPresent() ? NodeType.tenant : NodeType.host, new Reports(), Optional.empty(), Optional.empty()); } private Set<String> getAdditionalIP() { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerHostCapacityTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerHostCapacityTest.java index e2015cfd30a..7d9ac230771 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerHostCapacityTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerHostCapacityTest.java @@ -43,9 +43,9 @@ public class DockerHostCapacityTest { NodeFlavors nodeFlavors = FlavorConfigBuilder.createDummies("host", "docker", "docker2"); // Create three docker hosts - host1 = Node.create("host1", new IP.Config(Set.of("::1"), generateIPs(2, 4)), "host1", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), NodeType.host); - host2 = Node.create("host2", new IP.Config(Set.of("::11"), generateIPs(12, 3)), "host2", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), NodeType.host); - host3 = Node.create("host3", new IP.Config(Set.of("::21"), generateIPs(22, 1)), "host3", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), NodeType.host); + host1 = Node.create("host1", new IP.Config(Set.of("::1"), generateIPs(2, 4)), "host1", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), Optional.empty(), NodeType.host); + host2 = Node.create("host2", new IP.Config(Set.of("::11"), generateIPs(12, 3)), "host2", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), Optional.empty(), NodeType.host); + host3 = Node.create("host3", new IP.Config(Set.of("::21"), generateIPs(22, 1)), "host3", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("host"), Optional.empty(), NodeType.host); // Add two containers to host1 var nodeA = Node.createDockerNode(Set.of("::2"), "nodeA", "host1", resources1, NodeType.tenant); @@ -110,7 +110,7 @@ public class DockerHostCapacityTest { // Dev host can assign both configserver and tenant containers. var nodeFlavors = FlavorConfigBuilder.createDummies("devhost", "container"); - var devHost = Node.create("devhost", new IP.Config(Set.of("::1"), generateIPs(2, 10)), "devhost", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("devhost"), NodeType.devhost); + var devHost = Node.create("devhost", new IP.Config(Set.of("::1"), generateIPs(2, 10)), "devhost", Optional.empty(), Optional.empty(), nodeFlavors.getFlavorOrThrow("devhost"), Optional.empty(), NodeType.devhost); var cfg = Node.createDockerNode(Set.of("::2"), "cfg", "devhost", resources1, NodeType.config); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java index b731ab309a6..1e59add8304 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java @@ -14,6 +14,7 @@ import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.OutOfCapacityException; import com.yahoo.config.provision.ParentHostUnavailableException; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; @@ -107,6 +108,45 @@ public class DockerProvisioningTest { assertEquals(nodeCount, activeNodes.size()); } + @Test + public void reservations_are_respected() { + NodeResources resources = new NodeResources(10, 10, 10, 10); + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); + TenantName tenant1 = TenantName.from("tenant1"); + TenantName tenant2 = TenantName.from("tenant2"); + ApplicationId application1_1 = ApplicationId.from(tenant1, ApplicationName.from("application1"), InstanceName.defaultName()); + ApplicationId application2_1 = ApplicationId.from(tenant2, ApplicationName.from("application1"), InstanceName.defaultName()); + ApplicationId application2_2 = ApplicationId.from(tenant2, ApplicationName.from("application2"), InstanceName.defaultName()); + + tester.makeReadyNodes(10, resources, Optional.of(tenant1), NodeType.host, 1); + tester.makeReadyNodes(10, resources, Optional.empty(), NodeType.host, 1); + tester.deployZoneApp(); + + Version wantedVespaVersion = Version.fromString("6.39"); + List<HostSpec> nodes = tester.prepare(application2_1, + ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContent"), wantedVespaVersion, false), + 6, 1, resources); + assertHostSpecParentReservation(nodes, Optional.empty(), tester); // We do not get nodes on hosts reserved to tenant1 + tester.activate(application2_1, nodes); + + try { + tester.prepare(application2_2, + ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContent"), wantedVespaVersion, false), + 5, 1, resources); + fail("Expected exception"); + } + catch (OutOfCapacityException e) { + // Success: Not enough nonreserved hosts left + } + + nodes = tester.prepare(application1_1, + ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContent"), wantedVespaVersion, false), + 10, 1, resources); + assertHostSpecParentReservation(nodes, Optional.of(tenant1), tester); + tester.activate(application1_1, nodes); + assertNodeParentReservation(tester.getNodes(application1_1).asList(), Optional.empty(), tester); // Reservation is cleared after activation + } + /** Exclusive app first, then non-exclusive: Should give the same result as below */ @Test public void docker_application_deployment_with_exclusive_app_first() { @@ -260,4 +300,16 @@ public class DockerProvisioningTest { tester.activate(application, hosts); } + private void assertNodeParentReservation(List<Node> nodes, Optional<TenantName> reservation, ProvisioningTester tester) { + for (Node node : nodes) + assertEquals(reservation, tester.nodeRepository().getNode(node.parentHostname().get()).get().reservedTo()); + } + + private void assertHostSpecParentReservation(List<HostSpec> hostSpecs, Optional<TenantName> reservation, ProvisioningTester tester) { + for (HostSpec hostSpec : hostSpecs) { + Node node = tester.nodeRepository().getNode(hostSpec.hostname()).get(); + assertEquals(reservation, tester.nodeRepository().getNode(node.parentHostname().get()).get().reservedTo()); + } + } + } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java index d1498507a7c..53fecbf6095 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java @@ -423,7 +423,9 @@ public class DynamicDockerAllocationTest { } private void addAndAssignNode(ApplicationId id, String hostname, String parentHostname, ClusterSpec clusterSpec, NodeResources flavor, int index, ProvisioningTester tester) { - Node node1a = Node.create("open1", new IP.Config(Set.of("127.0.233." + index), Set.of()), hostname, Optional.of(parentHostname), Optional.empty(), new Flavor(flavor), NodeType.tenant); + Node node1a = Node.create("open1", new IP.Config(Set.of("127.0.233." + index), Set.of()), hostname, + Optional.of(parentHostname), Optional.empty(), new Flavor(flavor), Optional.empty(), NodeType.tenant + ); ClusterMembership clusterMembership1 = ClusterMembership.from( clusterSpec.with(Optional.of(ClusterSpec.Group.from(0))), index); // Need to add group here so that group is serialized in node allocation Node node1aAllocation = node1a.allocate(id, clusterMembership1, node1a.flavor().resources(), Instant.now()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java index c0e06ef3dff..9145cb9d88f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/InPlaceResizeProvisionTest.java @@ -47,6 +47,7 @@ import static org.junit.Assert.fail; * @author freva */ public class InPlaceResizeProvisionTest { + private static final NodeResources smallResources = new NodeResources(2, 4, 8, 1, NodeResources.DiskSpeed.any, NodeResources.StorageType.any); private static final NodeResources mediumResources = new NodeResources(4, 8, 16, 1, NodeResources.DiskSpeed.any, NodeResources.StorageType.any); private static final NodeResources largeResources = new NodeResources(8, 16, 32, 1, NodeResources.DiskSpeed.any, NodeResources.StorageType.any); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java index 6dfca4d2c04..a26e802dfe8 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java @@ -247,7 +247,7 @@ public class LoadBalancerProvisionerTest { } private void assignIps(List<Node> nodes) { - try (var lock = tester.nodeRepository().lockAllocation()) { + try (var lock = tester.nodeRepository().lockUnallocated()) { for (int i = 0; i < nodes.size(); i++) { tester.nodeRepository().write(nodes.get(i).with(IP.Config.EMPTY.with(Set.of("127.0.0." + i))), lock); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNodeTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNodeTest.java index 718abf7d73d..1d9c135b4b5 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNodeTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/PrioritizableNodeTest.java @@ -124,18 +124,26 @@ public class PrioritizableNodeTest { } private static Node node(String hostname, Node.State state) { - return new Node(hostname, new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.empty(), new Flavor(new NodeResources(2, 2, 2, 2)), - Status.initial(), state, Optional.empty(), History.empty(), NodeType.tenant, new Reports(), Optional.empty()); + return new Node(hostname, new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.empty(), + new Flavor(new NodeResources(2, 2, 2, 2)), + Status.initial(), state, Optional.empty(), History.empty(), NodeType.tenant, new Reports(), + Optional.empty(), Optional.empty()); } private static PrioritizableNode node(String hostname, NodeResources nodeResources, NodeResources allocatedHostResources, // allocated before adding nodeResources NodeResources totalHostResources) { - Node node = new Node(hostname, new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.of(hostname + "parent"), new Flavor(nodeResources), - Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.tenant, new Reports(), Optional.empty()); - Node parent = new Node(hostname + "parent", new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.empty(), new Flavor(totalHostResources), - Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.host, new Reports(), Optional.empty()); - return new PrioritizableNode(node, totalHostResources.subtract(allocatedHostResources), Optional.of(parent), false, false, true, false); + Node node = new Node(hostname, new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.of(hostname + "parent"), + new Flavor(nodeResources), + Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.tenant, + new Reports(), Optional.empty(), Optional.empty()); + Node parent = new Node(hostname + "parent", new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.empty(), + new Flavor(totalHostResources), + Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.host, + new Reports(), Optional.empty(), Optional.empty()); + return new PrioritizableNode(node, totalHostResources.subtract(allocatedHostResources), Optional.of(parent), + false, false, true, false); } + } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index 8be0da7c83d..4e63d3cc79f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -241,13 +241,16 @@ public class ProvisioningTester { } public List<Node> makeReadyNodes(int n, String flavor, NodeType type) { - return makeReadyNodes(n, asFlavor(flavor, type), type, 0); + return makeReadyNodes(n, asFlavor(flavor, type), Optional.empty(), type, 0); } public List<Node> makeReadyNodes(int n, NodeResources resources, NodeType type) { - return makeReadyNodes(n, new Flavor(resources), type, 0); + return makeReadyNodes(n, new Flavor(resources), Optional.empty(), type, 0); } public List<Node> makeReadyNodes(int n, NodeResources resources, NodeType type, int ipAddressPoolSize) { - return makeReadyNodes(n, new Flavor(resources), type, ipAddressPoolSize); + return makeReadyNodes(n, resources, Optional.empty(), type, ipAddressPoolSize); + } + public List<Node> makeReadyNodes(int n, NodeResources resources, Optional<TenantName> reservedTo, NodeType type, int ipAddressPoolSize) { + return makeReadyNodes(n, new Flavor(resources), reservedTo, type, ipAddressPoolSize); } public List<Node> makeProvisionedNodes(int count, String flavor, NodeType type, int ipAddressPoolSize) { @@ -255,9 +258,9 @@ public class ProvisioningTester { } public List<Node> makeProvisionedNodes(int n, String flavor, NodeType type, int ipAddressPoolSize, boolean dualStack) { - return makeProvisionedNodes(n, asFlavor(flavor, type), type, ipAddressPoolSize, dualStack); + return makeProvisionedNodes(n, asFlavor(flavor, type), Optional.empty(), type, ipAddressPoolSize, dualStack); } - public List<Node> makeProvisionedNodes(int n, Flavor flavor, NodeType type, int ipAddressPoolSize, boolean dualStack) { + public List<Node> makeProvisionedNodes(int n, Flavor flavor, Optional<TenantName> reservedTo, NodeType type, int ipAddressPoolSize, boolean dualStack) { List<Node> nodes = new ArrayList<>(n); for (int i = 0; i < n; i++) { @@ -299,6 +302,7 @@ public class ProvisioningTester { new IP.Config(hostIps, ipAddressPool), Optional.empty(), flavor, + reservedTo, type)); } nodes = nodeRepository.addNodes(nodes); @@ -315,11 +319,12 @@ public class ProvisioningTester { nameResolver.addRecord(hostname, ipv4); Node node = nodeRepository.createNode(hostname, - hostname, - new IP.Config(Set.of(ipv4), Set.of()), - Optional.empty(), - nodeFlavors.getFlavorOrThrow(flavor), - NodeType.config); + hostname, + new IP.Config(Set.of(ipv4), Set.of()), + Optional.empty(), + nodeFlavors.getFlavorOrThrow(flavor), + Optional.empty(), + NodeType.config); nodes.add(node); } @@ -338,17 +343,20 @@ public class ProvisioningTester { } public List<Node> makeReadyNodes(int n, String flavor, NodeType type, int ipAddressPoolSize) { - return makeReadyNodes(n, asFlavor(flavor, type), type, ipAddressPoolSize); + return makeReadyNodes(n, asFlavor(flavor, type), Optional.empty(), type, ipAddressPoolSize); } - public List<Node> makeReadyNodes(int n, Flavor flavor, NodeType type, int ipAddressPoolSize) { - return makeReadyNodes(n, flavor, type, ipAddressPoolSize, false); + public List<Node> makeReadyNodes(int n, Flavor flavor, Optional<TenantName> reservedTo, NodeType type, int ipAddressPoolSize) { + return makeReadyNodes(n, flavor, reservedTo, type, ipAddressPoolSize, false); } public List<Node> makeReadyNodes(int n, String flavor, NodeType type, int ipAddressPoolSize, boolean dualStack) { return makeReadyNodes(n, asFlavor(flavor, type), type, ipAddressPoolSize, dualStack); } public List<Node> makeReadyNodes(int n, Flavor flavor, NodeType type, int ipAddressPoolSize, boolean dualStack) { - List<Node> nodes = makeProvisionedNodes(n, flavor, type, ipAddressPoolSize, dualStack); + return makeReadyNodes(n, flavor, Optional.empty(), type, ipAddressPoolSize, dualStack); + } + public List<Node> makeReadyNodes(int n, Flavor flavor, Optional<TenantName> reservedTo, NodeType type, int ipAddressPoolSize, boolean dualStack) { + List<Node> nodes = makeProvisionedNodes(n, flavor, reservedTo, type, ipAddressPoolSize, dualStack); nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); return nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/RestApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/RestApiTest.java index 18f0d989900..1a1cbdea6b8 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/RestApiTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/RestApiTest.java @@ -6,6 +6,7 @@ import com.yahoo.application.container.JDisc; import com.yahoo.application.container.handler.Request; import com.yahoo.application.container.handler.Response; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.TenantName; import com.yahoo.io.IOUtils; import com.yahoo.text.Utf8; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -23,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -106,7 +108,7 @@ public class RestApiTest { assertResponse(new Request("http://localhost:8080/nodes/v2/node", ("[" + asNodeJson("host8.yahoo.com", "default", "127.0.8.1") + "," + // test with only 1 ip address asNodeJson("host9.yahoo.com", "large-variant", "127.0.9.1", "::9:1") + "," + - asHostJson("parent2.yahoo.com", "large-variant", "127.0.127.1", "::127:1") + "," + + asHostJson("parent2.yahoo.com", "large-variant", Optional.of(TenantName.from("myTenant")), "127.0.127.1", "::127:1") + "," + asDockerNodeJson("host11.yahoo.com", "parent.host.yahoo.com", "::11") + "]"). getBytes(StandardCharsets.UTF_8), Request.Method.POST), @@ -292,7 +294,7 @@ public class RestApiTest { // Attempt to POST host node with already assigned IP assertResponse(new Request("http://localhost:8080/nodes/v2/node", - "[" + asHostJson("host200.yahoo.com", "default", "127.0.2.1") + "]", + "[" + asHostJson("host200.yahoo.com", "default", Optional.empty(), "127.0.2.1") + "]", Request.Method.POST), 400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Cannot assign [127.0.2.1] to host200.yahoo.com: [127.0.2.1] already assigned to host2.yahoo.com\"}"); @@ -304,7 +306,7 @@ public class RestApiTest { // Node types running a single container can share their IP address with child node assertResponse(new Request("http://localhost:8080/nodes/v2/node", - "[" + asNodeJson("cfghost42.yahoo.com", NodeType.confighost, "default", "127.0.42.1") + "]", + "[" + asNodeJson("cfghost42.yahoo.com", NodeType.confighost, "default", Optional.empty(), "127.0.42.1") + "]", Request.Method.POST), 200, "{\"message\":\"Added 1 nodes to the provisioned state\"}"); assertResponse(new Request("http://localhost:8080/nodes/v2/node", @@ -320,7 +322,7 @@ public class RestApiTest { // ... nor with child node on different host assertResponse(new Request("http://localhost:8080/nodes/v2/node", - "[" + asNodeJson("cfghost43.yahoo.com", NodeType.confighost, "default", "127.0.43.1") + "]", + "[" + asNodeJson("cfghost43.yahoo.com", NodeType.confighost, "default", Optional.empty(), "127.0.43.1") + "]", Request.Method.POST), 200, "{\"message\":\"Added 1 nodes to the provisioned state\"}"); assertResponse(new Request("http://localhost:8080/nodes/v2/node/cfg42.yahoo.com", @@ -921,14 +923,15 @@ public class RestApiTest { "\"flavor\":\"" + flavor + "\"}"; } - private static String asHostJson(String hostname, String flavor, String... ipAddress) { - return asNodeJson(hostname, NodeType.host, flavor, ipAddress); + private static String asHostJson(String hostname, String flavor, Optional<TenantName> reservedTo, String... ipAddress) { + return asNodeJson(hostname, NodeType.host, flavor, reservedTo, ipAddress); } - private static String asNodeJson(String hostname, NodeType nodeType, String flavor, String... ipAddress) { + private static String asNodeJson(String hostname, NodeType nodeType, String flavor, Optional<TenantName> reservedTo, String... ipAddress) { return "{\"hostname\":\"" + hostname + "\", \"openStackId\":\"" + hostname + "\"," + createIpAddresses(ipAddress) + "\"flavor\":\"" + flavor + "\"" + + (reservedTo.isPresent() ? ", \"reservedTo\":\"" + reservedTo.get().value() + "\"" : "") + ", \"type\":\"" + nodeType + "\"}"; } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json index 8be034cb036..ecc497172c7 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/parent2.json @@ -6,6 +6,7 @@ "hostname": "parent2.yahoo.com", "openStackId": "parent2.yahoo.com", "flavor": "large-variant", + "reservedTo": "myTenant", "cpuCores": 64.0, "resources": {"vcpu":64.0,"memoryGb":128.0,"diskGb":2000.0,"bandwidthGbps":15.0,"diskSpeed":"fast","storageType":"remote"}, "environment": "BARE_METAL", |