diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2021-07-27 16:02:01 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-27 16:02:01 +0200 |
commit | 8a450d4b8d7553c37a6361711b424df01a876637 (patch) | |
tree | a0ebc8196259dff8d0c8a3415ecdb637a2088003 | |
parent | 048672327aac65c13b29cf6b3a1076496d2b60c7 (diff) | |
parent | 72099d6016f7a4fd4b0149d61562cde8e5197004 (diff) |
Merge pull request #18648 from vespa-engine/freva/shared-cc
Support reserving hosts to nodes of given cluster type
23 files changed, 194 insertions, 79 deletions
diff --git a/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java b/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java index 129e1020b04..a4e25288d1d 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/custom/HostResources.java @@ -2,10 +2,12 @@ package com.yahoo.vespa.flags.custom; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Objects; +import java.util.Optional; import java.util.Set; /** @@ -18,19 +20,18 @@ import java.util.Set; public class HostResources { private static final Set<String> validDiskSpeeds = Set.of("slow", "fast"); private static final Set<String> validStorageTypes = Set.of("remote", "local"); + private static final Set<String> validClusterTypes = Set.of("container", "content", "combined", "admin"); private final double vcpu; - private final double memoryGb; - private final double diskGb; - private final double bandwidthGbps; private final String diskSpeed; - private final String storageType; + private final Optional<String> clusterType; + private final int containers; @JsonCreator @@ -40,6 +41,7 @@ public class HostResources { @JsonProperty("bandwidthGbps") Double bandwidthGbps, @JsonProperty("diskSpeed") String diskSpeed, @JsonProperty("storageType") String storageType, + @JsonProperty("clusterType") String clusterType, @JsonProperty("containers") Integer containers) { this.vcpu = requirePositive("vcpu", vcpu); this.memoryGb = requirePositive("memoryGb", memoryGb); @@ -47,6 +49,7 @@ public class HostResources { this.bandwidthGbps = requirePositive("bandwidthGbps", bandwidthGbps); this.diskSpeed = validateEnum("diskSpeed", validDiskSpeeds, diskSpeed); this.storageType = validateEnum("storageType", validStorageTypes, storageType); + this.clusterType = Optional.ofNullable(clusterType).map(cType -> validateEnum("clusterType", validClusterTypes, cType)); this.containers = requirePositive("containers", containers); } @@ -68,9 +71,19 @@ public class HostResources { @JsonProperty("storageType") public String storageType() { return storageType; } + @JsonProperty("clusterType") + public String clusterTypeOrNull() { return clusterType.orElse(null); } + + @JsonIgnore + public Optional<String> clusterType() { return clusterType; } + @JsonProperty("containers") public int containers() { return containers; } + public boolean satisfiesClusterType(String clusterType) { + return this.clusterType.map(clusterType::equalsIgnoreCase).orElse(true); + } + private static double requirePositive(String name, Double value) { requireNonNull(name, value); if (value <= 0) @@ -106,6 +119,7 @@ public class HostResources { ", bandwidthGbps=" + bandwidthGbps + ", diskSpeed='" + diskSpeed + '\'' + ", storageType='" + storageType + '\'' + + ", clusterType='" + clusterType + '\'' + ", containers=" + containers + '}'; } @@ -121,11 +135,12 @@ public class HostResources { Double.compare(resources.bandwidthGbps, bandwidthGbps) == 0 && diskSpeed.equals(resources.diskSpeed) && storageType.equals(resources.storageType) && + clusterType.equals(resources.clusterType) && containers == resources.containers; } @Override public int hashCode() { - return Objects.hash(vcpu, memoryGb, diskGb, bandwidthGbps, diskSpeed, storageType, containers); + return Objects.hash(vcpu, memoryGb, diskGb, bandwidthGbps, diskSpeed, storageType, clusterType, containers); } } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java b/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java index c952161cf72..afbb2ce00b3 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/custom/SharedHost.java @@ -52,8 +52,8 @@ public class SharedHost { } @JsonIgnore - public boolean isEnabled() { - return resources.size() > 0; + public boolean isEnabled(String clusterType) { + return resources.stream().anyMatch(hr -> hr.satisfiesClusterType(clusterType)); } @JsonIgnore diff --git a/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java b/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java index 956048f6e24..32098ca4cce 100644 --- a/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java +++ b/flags/src/test/java/com/yahoo/vespa/flags/PermanentFlagsTest.java @@ -17,7 +17,7 @@ class PermanentFlagsTest { public void testSharedHostFlag() { SharedHost sharedHost = new SharedHost(List.of(new HostResources( 4.0, 16.0, 50.0, 0.3, - "fast", "local", + "fast", "local", "admin", 10)), null); testGeneric(PermanentFlags.SHARED_HOST, sharedHost); diff --git a/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java b/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java index f0a11f244a4..2c78dda48e5 100644 --- a/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java +++ b/flags/src/test/java/com/yahoo/vespa/flags/custom/SharedHostTest.java @@ -12,8 +12,8 @@ import static org.junit.Assert.assertEquals; public class SharedHostTest { @Test public void serialization() throws IOException { - verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", 5)), 6)); - verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", 5)), null)); + verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", "container", 5)), 6)); + verifySerialization(new SharedHost(List.of(new HostResources(1.0, 2.0, 3.0, 4.0, "fast", "remote", "admin", 5)), null)); } private void verifySerialization(SharedHost sharedHost) throws IOException { 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 3dbafdc2aba..240e041a504 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 @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -46,7 +47,8 @@ public final class Node implements Nodelike { private final Reports reports; private final Optional<String> modelName; private final Optional<TenantName> reservedTo; - private final Optional<ApplicationId> exclusiveTo; + private final Optional<ApplicationId> exclusiveToApplicationId; + private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final Optional<String> switchHostname; /** Record of the last event of each type happening to this node */ @@ -76,7 +78,8 @@ public final class Node implements Nodelike { 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, Optional<TenantName> reservedTo, - Optional<ApplicationId> exclusiveTo, Optional<String> switchHostname) { + Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, + Optional<String> switchHostname) { 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"); @@ -90,7 +93,8 @@ public final class Node implements Nodelike { 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"); - this.exclusiveTo = Objects.requireNonNull(exclusiveTo, "exclusiveTo cannot be null"); + this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId cannot be null"); + this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType cannot be null"); this.switchHostname = requireNonEmptyString(switchHostname, "switchHostname cannot be null"); if (state == State.active) @@ -110,8 +114,11 @@ public final class Node implements Nodelike { if (type != NodeType.host && reservedTo.isPresent()) throw new IllegalArgumentException("Only tenant hosts can be reserved to a tenant"); - if (type != NodeType.host && exclusiveTo.isPresent()) + if (type != NodeType.host && exclusiveToApplicationId.isPresent()) throw new IllegalArgumentException("Only tenant hosts can be exclusive to an application"); + + if (type != NodeType.host && exclusiveToClusterType.isPresent()) + throw new IllegalArgumentException("Only tenant hosts can be exclusive to a cluster type"); } /** Returns the IP config of this node */ @@ -182,11 +189,18 @@ public final class Node implements Nodelike { public Optional<TenantName> reservedTo() { return reservedTo; } /** - * Returns the application this node is exclusive to, if any. Only hosts can be exclusive to an application. + * Returns the application this host is exclusive to, if any. Only tenant hosts can be exclusive to an application. * If this is set, resources on this host cannot be allocated to any other application. This is set during * provisioning and applies for the entire lifetime of the host */ - public Optional<ApplicationId> exclusiveTo() { return exclusiveTo; } + public Optional<ApplicationId> exclusiveToApplicationId() { return exclusiveToApplicationId; } + + /** + * Returns the cluster type this host is exclusive to, if any. Only tenant hosts can be exclusive to a cluster type. + * If this is set, resources on this host cannot be allocated to any other cluster type. This is set during + * provisioning and applies for the entire lifetime of the host + */ + public Optional<ClusterSpec.Type> exclusiveToClusterType() { return exclusiveToClusterType; } /** Returns the hostname of the switch this node is connected to, if any */ public Optional<String> switchHostname() { @@ -281,13 +295,13 @@ public final class Node implements Nodelike { /** 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, reservedTo, exclusiveTo, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** Returns a node with the flavor assigned to the given value */ @@ -295,31 +309,31 @@ public final class Node implements Nodelike { if (flavor.equals(this.flavor)) return this; History updateHistory = history.with(new History.Event(History.Event.Type.resized, agent, instant)); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, allocation, updateHistory, type, - reports, modelName, reservedTo, exclusiveTo, switchHostname); + reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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), reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, Optional.of(modelName), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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(), reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, Optional.empty(), reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** Returns a copy of this with a history record saying it was detected to be down at this instant */ @@ -350,50 +364,55 @@ public final class Node implements Nodelike { */ public Node with(Allocation allocation) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveTo, switchHostname); + Optional.of(allocation), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + Optional.empty(), history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } public Node withReservedTo(TenantName tenant) { if (type != NodeType.host) throw new IllegalArgumentException("Only host nodes can be reserved, " + hostname + " has type " + type); return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, Optional.of(tenant), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } /** 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(), exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, Optional.empty(), exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } - public Node withExclusiveTo(ApplicationId exclusiveTo) { + public Node withExclusiveToApplicationId(ApplicationId exclusiveTo) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), switchHostname); + allocation, history, type, reports, modelName, reservedTo, Optional.ofNullable(exclusiveTo), exclusiveToClusterType, switchHostname); + } + + public Node withExclusiveToClusterType(ClusterSpec.Type exclusiveTo) { + return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, Optional.ofNullable(exclusiveTo), switchHostname); } /** Returns a copy of this node with switch hostname set to given value */ public Node withSwitchHostname(String switchHostname) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveTo, Optional.ofNullable(switchHostname)); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, Optional.ofNullable(switchHostname)); } /** Returns a copy of this node with switch hostname unset */ @@ -431,12 +450,12 @@ public final class Node implements Nodelike { /** 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, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } public Node with(Reports reports) { return new Node(id, ipConfig, hostname, parentHostname, flavor, status, state, - allocation, history, type, reports, modelName, reservedTo, exclusiveTo, switchHostname); + allocation, history, type, reports, modelName, reservedTo, exclusiveToApplicationId, exclusiveToClusterType, switchHostname); } private static Optional<String> requireNonEmptyString(Optional<String> value, String message) { @@ -567,7 +586,8 @@ public final class Node implements Nodelike { private String parentHostname; private String modelName; private TenantName reservedTo; - private ApplicationId exclusiveTo; + private ApplicationId exclusiveToApplicationId; + private ClusterSpec.Type exclusiveToClusterType; private String switchHostname; private Allocation allocation; private IP.Config ipConfig; @@ -598,8 +618,13 @@ public final class Node implements Nodelike { return this; } - public Builder exclusiveTo(ApplicationId exclusiveTo) { - this.exclusiveTo = exclusiveTo; + public Builder exclusiveToApplicationId(ApplicationId exclusiveTo) { + this.exclusiveToApplicationId = exclusiveTo; + return this; + } + + public Builder exclusiveToClusterType(ClusterSpec.Type exclusiveTo) { + this.exclusiveToClusterType = exclusiveTo; return this; } @@ -642,8 +667,8 @@ public final class Node implements Nodelike { return new Node(id, Optional.ofNullable(ipConfig).orElse(IP.Config.EMPTY), hostname, Optional.ofNullable(parentHostname), flavor, Optional.ofNullable(status).orElseGet(Status::initial), state, Optional.ofNullable(allocation), Optional.ofNullable(history).orElseGet(History::empty), type, Optional.ofNullable(reports).orElseGet(Reports::new), - Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveTo), - Optional.ofNullable(switchHostname)); + Optional.ofNullable(modelName), Optional.ofNullable(reservedTo), Optional.ofNullable(exclusiveToApplicationId), + Optional.ofNullable(exclusiveToClusterType), Optional.ofNullable(switchHostname)); } } 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 0eb2038e233..78686d8aeed 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 @@ -206,7 +206,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { return nodeList.stream() .filter(node -> Nodes.canAllocateTenantNodeTo(node, true)) .filter(node -> node.reservedTo().isEmpty()) - .filter(node -> node.exclusiveTo().isEmpty()) + .filter(node -> node.exclusiveToApplicationId().isEmpty()) .collect(Collectors.toMap(Node::hostname, Function.identity())); } @@ -245,7 +245,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { Version osVersion = nodeRepository().osVersions().targetFor(NodeType.host).orElse(Version.emptyVersion); List<Integer> provisionIndices = nodeRepository().database().readProvisionIndices(count); List<Node> hosts = hostProvisioner.provisionHosts(provisionIndices, NodeType.host, nodeResources, - ApplicationId.defaultId(), osVersion, HostSharing.shared) + ApplicationId.defaultId(), osVersion, HostSharing.shared, Optional.empty()) .stream() .map(ProvisionedHost::generateHost) .collect(Collectors.toList()); 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 da4ab528630..d5a4c459ef3 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 @@ -210,7 +210,7 @@ public class CuratorDatabaseClient { toState.isAllocated() ? node.allocation() : Optional.empty(), node.history().recordStateTransition(node.state(), toState, agent, clock.instant()), node.type(), node.reports(), node.modelName(), node.reservedTo(), - node.exclusiveTo(), node.switchHostname()); + node.exclusiveToApplicationId(), node.exclusiveToClusterType(), node.switchHostname()); 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 dff4a66bd42..19bbe92eff6 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 @@ -10,6 +10,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.InstanceName; @@ -91,7 +92,8 @@ public class NodeSerializer { private static final String reportsKey = "reports"; private static final String modelNameKey = "modelName"; private static final String reservedToKey = "reservedTo"; - private static final String exclusiveToKey = "exclusiveTo"; + private static final String exclusiveToApplicationIdKey = "exclusiveTo"; + private static final String exclusiveToClusterTypeKey = "exclusiveToClusterType"; private static final String switchHostnameKey = "switchHostname"; // Node resource fields @@ -178,7 +180,8 @@ public class NodeSerializer { node.reports().toSlime(object, reportsKey); node.modelName().ifPresent(modelName -> object.setString(modelNameKey, modelName)); node.reservedTo().ifPresent(tenant -> object.setString(reservedToKey, tenant.value())); - node.exclusiveTo().ifPresent(applicationId -> object.setString(exclusiveToKey, applicationId.serializedForm())); + node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString(exclusiveToApplicationIdKey, applicationId.serializedForm())); + node.exclusiveToClusterType().ifPresent(clusterType -> object.setString(exclusiveToClusterTypeKey, clusterType.name())); } private void toSlime(Flavor flavor, Cursor object) { @@ -264,7 +267,8 @@ public class NodeSerializer { Reports.fromSlime(object.field(reportsKey)), modelNameFromSlime(object), reservedToFromSlime(object.field(reservedToKey)), - exclusiveToFromSlime(object.field(exclusiveToKey)), + exclusiveToApplicationIdFromSlime(object.field(exclusiveToApplicationIdKey)), + exclusiveToClusterTypeFromSlime(object.field(exclusiveToClusterTypeKey)), switchHostnameFromSlime(object.field(switchHostnameKey))); } @@ -401,13 +405,20 @@ public class NodeSerializer { return Optional.of(TenantName.from(object.asString())); } - private Optional<ApplicationId> exclusiveToFromSlime(Inspector object) { + private Optional<ApplicationId> exclusiveToApplicationIdFromSlime(Inspector object) { if (! object.valid()) return Optional.empty(); if (object.type() != Type.STRING) throw new IllegalArgumentException("Expected 'exclusiveTo' to be a string but is " + object); return Optional.of(ApplicationId.fromSerializedForm(object.asString())); } + private Optional<ClusterSpec.Type> exclusiveToClusterTypeFromSlime(Inspector object) { + if (! object.valid()) return Optional.empty(); + if (object.type() != Type.STRING) + throw new IllegalArgumentException("Expected 'exclusiveToClusterType' to be a string but is " + object); + return Optional.of(ClusterSpec.Type.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/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java index abd910485ac..868f554e2ff 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 @@ -11,7 +11,7 @@ import com.yahoo.config.provision.Zone; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.provision.NodeRepository; -import java.util.function.Supplier; +import java.util.function.Function; /** * Defines the policies for assigning cluster capacity in various environments @@ -22,11 +22,11 @@ import java.util.function.Supplier; public class CapacityPolicies { private final Zone zone; - private final Supplier<Boolean> sharedHosts; + private final Function<ClusterSpec.Type, Boolean> sharedHosts; public CapacityPolicies(NodeRepository nodeRepository) { this.zone = nodeRepository.zone(); - this.sharedHosts = PermanentFlags.SHARED_HOST.bindTo(nodeRepository.flagSource()).value()::isEnabled; + this.sharedHosts = type -> PermanentFlags.SHARED_HOST.bindTo(nodeRepository.flagSource()).value().isEnabled(type.name()); } public int decideSize(int requested, Capacity capacity, ClusterSpec cluster, ApplicationId application) { @@ -66,7 +66,7 @@ public class CapacityPolicies { // Use small logserver in dev system return new NodeResources(0.1, 1, 10, 0.3); } - return zone.getCloud().dynamicProvisioning() && ! sharedHosts.get() ? + return zone.getCloud().dynamicProvisioning() && ! sharedHosts.apply(clusterType) ? new NodeResources(0.5, 4, 50, 0.3) : new NodeResources(0.5, 2, 50, 0.3); } 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 5d45bed19e8..aad32f64fa8 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 @@ -81,7 +81,8 @@ public class GroupPreparer { hostType, deficit.resources(), application, osVersion, - sharing)) + sharing, + Optional.of(cluster.type()))) .orElseGet(List::of); // At this point we have started provisioning of the hosts, the first priority is to make sure that diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java index bfb526a518f..77bee2b346e 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/HostProvisioner.java @@ -3,11 +3,13 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.Node; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -48,7 +50,8 @@ public interface HostProvisioner { NodeResources resources, ApplicationId applicationId, Version osVersion, - HostSharing sharing); + HostSharing sharing, + Optional<ClusterSpec.Type> clusterType); /** * Continue provisioning of given list of Nodes. 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 b5579451a0e..bdb7cb4b64d 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 @@ -189,10 +189,13 @@ class NodeAllocation { private boolean violatesExclusivity(NodeCandidate candidate) { if (candidate.parentHostname().isEmpty()) return false; - // In dynamic provisioned zones a node requiring exclusivity must be on a host that has exclusiveTo equal to its owner - if (nodeRepository.zone().getCloud().dynamicProvisioning()) - return requestedNodes.isExclusive() && - ! candidate.parent.flatMap(Node::exclusiveTo).map(application::equals).orElse(false); + // In dynamic provisioned zones, exclusivity is violated if... + if (nodeRepository.zone().getCloud().dynamicProvisioning()) { + // If either the parent is dedicated to a cluster type different from this cluster + return ! candidate.parent.flatMap(Node::exclusiveToClusterType).map(cluster.type()::equals).orElse(true) || + // or this cluster is requiring exclusivity, but the host is exclusive to a different owner + (requestedNodes.isExclusive() && !candidate.parent.flatMap(Node::exclusiveToApplicationId).map(application::equals).orElse(false)); + } // In non-dynamic provisioned zones we require that if either of the nodes on the host requires exclusivity, // then all the nodes on the host must have the same owner 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 5fe10f09f8a..6568046991b 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 @@ -136,7 +136,8 @@ public class NodePrioritizer { if ( ! Nodes.canAllocateTenantNodeTo(host, dynamicProvisioning)) continue; if (host.reservedTo().isPresent() && !host.reservedTo().get().equals(application.tenant())) continue; if (host.reservedTo().isPresent() && application.instance().isTester()) continue; - if (host.exclusiveTo().isPresent()) continue; // Never allocate new nodes to exclusive hosts + if (host.exclusiveToApplicationId().isPresent()) continue; // Never allocate new nodes to exclusive hosts + if ( ! host.exclusiveToClusterType().map(clusterSpec.type()::equals).orElse(true)) continue; if (spareHosts.contains(host) && !canAllocateToSpareHosts) continue; if ( ! capacity.hasCapacity(host, requestedNodes.resources().get())) continue; if ( ! allNodes.childrenOf(host).owner(application).cluster(clusterSpec.id()).isEmpty()) continue; @@ -183,7 +184,7 @@ public class NodePrioritizer { spareHosts.contains(parent.get()), isSurplus, false, - parent.get().exclusiveTo().isEmpty() + parent.get().exclusiveToApplicationId().isEmpty() && requestedNodes.canResize(node.resources(), capacity.freeCapacityOf(parent.get(), false), topologyChange, diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java index 2b64cc86c9a..0b007bf4a41 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeResourceLimits.java @@ -43,7 +43,7 @@ public class NodeResourceLimits { if (candidateNode.type() != NodeType.tenant) return true; // Resource limits only apply to tenant nodes // This node is allocated exclusively if that has been explicitly requested, or if the host of the node was // provisioned exclusively - boolean exclusive = cluster.isExclusive() || candidateNode.parent.flatMap(Node::exclusiveTo).isPresent(); + boolean exclusive = cluster.isExclusive() || candidateNode.parent.flatMap(Node::exclusiveToApplicationId).isPresent(); return isWithinRealLimits(nodeRepository.resourcesCalculator().realResourcesOf(candidateNode, nodeRepository, exclusive), cluster.type()); } 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 caaea1167b5..f0eb4a59af8 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 @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -28,18 +29,21 @@ public class ProvisionedHost { private final String hostHostname; private final Flavor hostFlavor; private final NodeType hostType; - private final Optional<ApplicationId> exclusiveTo; + private final Optional<ApplicationId> exclusiveToApplicationId; + private final Optional<ClusterSpec.Type> exclusiveToClusterType; private final List<Address> nodeAddresses; private final NodeResources nodeResources; private final Version osVersion; - public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType, Optional<ApplicationId> exclusiveTo, + public ProvisionedHost(String id, String hostHostname, Flavor hostFlavor, NodeType hostType, + Optional<ApplicationId> exclusiveToApplicationId, Optional<ClusterSpec.Type> exclusiveToClusterType, List<Address> nodeAddresses, NodeResources nodeResources, Version osVersion) { this.id = Objects.requireNonNull(id, "Host id must be set"); this.hostHostname = Objects.requireNonNull(hostHostname, "Host hostname must be set"); this.hostFlavor = Objects.requireNonNull(hostFlavor, "Host flavor must be set"); this.hostType = Objects.requireNonNull(hostType, "Host type must be set"); - this.exclusiveTo = Objects.requireNonNull(exclusiveTo, "exclusiveTo must be set"); + this.exclusiveToApplicationId = Objects.requireNonNull(exclusiveToApplicationId, "exclusiveToApplicationId must be set"); + this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType must be set"); this.nodeAddresses = validateNodeAddresses(nodeAddresses); this.nodeResources = Objects.requireNonNull(nodeResources, "Node resources must be set"); this.osVersion = Objects.requireNonNull(osVersion, "OS version must be set"); @@ -59,7 +63,8 @@ public class ProvisionedHost { Node.Builder builder = Node .create(id, IP.Config.of(Set.of(), Set.of(), nodeAddresses), hostHostname, hostFlavor, hostType) .status(Status.initial().withOsVersion(OsVersion.EMPTY.withCurrent(Optional.of(osVersion)))); - exclusiveTo.ifPresent(builder::exclusiveTo); + exclusiveToApplicationId.ifPresent(builder::exclusiveToApplicationId); + exclusiveToClusterType.ifPresent(builder::exclusiveToClusterType); return builder.build(); } @@ -98,6 +103,9 @@ public class ProvisionedHost { return id.equals(that.id) && hostHostname.equals(that.hostHostname) && hostFlavor.equals(that.hostFlavor) && + hostType == that.hostType && + exclusiveToApplicationId.equals(that.exclusiveToApplicationId) && + exclusiveToClusterType.equals(that.exclusiveToClusterType) && nodeAddresses.equals(that.nodeAddresses) && nodeResources.equals(that.nodeResources) && osVersion.equals(that.osVersion); @@ -105,7 +113,7 @@ public class ProvisionedHost { @Override public int hashCode() { - return Objects.hash(id, hostHostname, hostFlavor, nodeAddresses, nodeResources, osVersion); + return Objects.hash(id, hostHostname, hostFlavor, hostType, exclusiveToApplicationId, exclusiveToClusterType, nodeAddresses, nodeResources, osVersion); } @Override @@ -114,7 +122,10 @@ public class ProvisionedHost { "id='" + id + '\'' + ", hostHostname='" + hostHostname + '\'' + ", hostFlavor=" + hostFlavor + - ", nodeAddresses='" + nodeAddresses + '\'' + + ", hostType=" + hostType + + ", exclusiveToApplicationId=" + exclusiveToApplicationId + + ", exclusiveToClusterType=" + exclusiveToClusterType + + ", nodeAddresses=" + nodeAddresses + ", nodeResources=" + nodeResources + ", osVersion=" + osVersion + '}'; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java index 1bea7056790..8d37c13d2bc 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodePatcher.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.restapi; import com.google.common.base.Suppliers; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.NodeFlavors; import com.yahoo.config.provision.NodeResources; @@ -179,7 +180,10 @@ public class NodePatcher implements AutoCloseable { case "reservedTo": return value.type() == Type.NIX ? node.withoutReservedTo() : node.withReservedTo(TenantName.from(value.asString())); case "exclusiveTo": - return node.withExclusiveTo(SlimeUtils.optionalString(value).map(ApplicationId::fromSerializedForm).orElse(null)); + case "exclusiveToApplicationId": + return node.withExclusiveToApplicationId(SlimeUtils.optionalString(value).map(ApplicationId::fromSerializedForm).orElse(null)); + case "exclusiveToClusterType": + return node.withExclusiveToClusterType(SlimeUtils.optionalString(value).map(ClusterSpec.Type::valueOf).orElse(null)); case "switchHostname": return value.type() == Type.NIX ? node.withoutSwitchHostname() : node.withSwitchHostname(value.asString()); default : diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java index 1b3b2f81f11..53b6a91d962 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesResponse.java @@ -129,7 +129,8 @@ class NodesResponse extends SlimeJsonResponse { object.setString("openStackId", node.id()); object.setString("flavor", node.flavor().name()); node.reservedTo().ifPresent(reservedTo -> object.setString("reservedTo", reservedTo.value())); - node.exclusiveTo().ifPresent(exclusiveTo -> object.setString("exclusiveTo", exclusiveTo.serializedForm())); + node.exclusiveToApplicationId().ifPresent(applicationId -> object.setString("exclusiveTo", applicationId.serializedForm())); + node.exclusiveToClusterType().ifPresent(clusterType -> object.setString("exclusiveToClusterType", clusterType.name())); if (node.flavor().isConfigured()) object.setDouble("cpuCores", node.flavor().resources().vcpu()); NodeResourcesSerializer.toSlime(node.flavor().resources(), object.setObject("resources")); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index 24e297de179..c35d6b89090 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -283,7 +283,7 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { optionalString(inspector.field("parentHostname")).ifPresent(builder::parentHostname); optionalString(inspector.field("modelName")).ifPresent(builder::modelName); optionalString(inspector.field("reservedTo")).map(TenantName::from).ifPresent(builder::reservedTo); - optionalString(inspector.field("exclusiveTo")).map(ApplicationId::fromSerializedForm).ifPresent(builder::exclusiveTo); + optionalString(inspector.field("exclusiveTo")).map(ApplicationId::fromSerializedForm).ifPresent(builder::exclusiveToApplicationId); optionalString(inspector.field("switchHostname")).ifPresent(builder::switchHostname); return builder.build(); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java index 25e74df677b..50585c19946 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.testutils; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; @@ -54,7 +55,8 @@ public class MockHostProvisioner implements HostProvisioner { @Override public List<ProvisionedHost> provisionHosts(List<Integer> provisionIndices, NodeType hostType, NodeResources resources, - ApplicationId applicationId, Version osVersion, HostSharing sharing) { + ApplicationId applicationId, Version osVersion, HostSharing sharing, + Optional<ClusterSpec.Type> clusterType) { Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream().filter(f -> compatible(f, resources)) .findFirst() .orElseThrow(() -> new OutOfCapacityException("No host flavor matches " + resources))); @@ -66,6 +68,7 @@ public class MockHostProvisioner implements HostProvisioner { hostFlavor, hostType, Optional.empty(), + Optional.empty(), createAddressesForHost(hostType, hostFlavor, index), resources, osVersion)); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java index 523ceeb94ce..158a1d6e5ac 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializerTest.java @@ -7,6 +7,7 @@ import com.yahoo.component.Vtag; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NetworkPorts; import com.yahoo.config.provision.NodeFlavors; @@ -450,12 +451,15 @@ public class NodeSerializerTest { Node.Builder builder = Node.create("myId", IP.Config.EMPTY, "myHostname", nodeFlavors.getFlavorOrThrow("default"), NodeType.host); Node node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(builder.build())); - assertFalse(node.exclusiveTo().isPresent()); + assertFalse(node.exclusiveToApplicationId().isPresent()); + assertFalse(node.exclusiveToClusterType().isPresent()); - ApplicationId exclusiveTo = ApplicationId.from("tenant1", "app1", "instance1"); - node = builder.exclusiveTo(exclusiveTo).build(); + ApplicationId exclusiveToApp = ApplicationId.from("tenant1", "app1", "instance1"); + ClusterSpec.Type exclusiveToCluster = ClusterSpec.Type.admin; + node = builder.exclusiveToApplicationId(exclusiveToApp).exclusiveToClusterType(exclusiveToCluster).build(); node = nodeSerializer.fromJson(State.provisioned, nodeSerializer.toJson(node)); - assertEquals(exclusiveTo, node.exclusiveTo().get()); + assertEquals(exclusiveToApp, node.exclusiveToApplicationId().get()); + assertEquals(exclusiveToCluster, node.exclusiveToClusterType().get()); } private byte[] createNodeJson(String hostname, String... ipAddress) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java index 0b7b9f2fa13..3a7c61f68a1 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicProvisioningTest.java @@ -28,6 +28,7 @@ import org.junit.Test; import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -72,7 +73,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, null); // Provision shared hosts prepareAndActivate(application1, clusterSpec("mycluster"), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(100, 101, 102, 103), NodeType.host, resources, application1, - Version.emptyVersion, HostSharing.any); + Version.emptyVersion, HostSharing.any, Optional.of(ClusterSpec.Type.content)); // Total of 8 nodes should now be in node-repo, 4 active hosts and 4 active nodes assertEquals(8, tester.nodeRepository().nodes().list().size()); @@ -96,7 +97,7 @@ public class DynamicProvisioningTest { mockHostProvisioner(hostProvisioner, "large", 3, application3); prepareAndActivate(application3, clusterSpec("mycluster", true), 4, 1, resources); verify(hostProvisioner).provisionHosts(List.of(104, 105, 106, 107), NodeType.host, resources, application3, - Version.emptyVersion, HostSharing.exclusive); + Version.emptyVersion, HostSharing.exclusive, Optional.of(ClusterSpec.Type.content)); // Total of 20 nodes should now be in node-repo, 8 active hosts and 12 active nodes assertEquals(20, tester.nodeRepository().nodes().list().size()); @@ -468,7 +469,7 @@ public class DynamicProvisioningTest { }).collect(Collectors.toSet()); Node parent = Node.create(hostHostname, new IP.Config(Set.of(hostIp), pool), hostHostname, hostFlavor, NodeType.host) - .exclusiveTo(exclusiveTo).build(); + .exclusiveToApplicationId(exclusiveTo).build(); Node child = Node.reserve(Set.of("::" + hostIndex + ":1"), hostHostname + "-1", hostHostname, nodeResources, NodeType.tenant).build(); ProvisionedHost provisionedHost = mock(ProvisionedHost.class); when(provisionedHost.generateHost()).thenReturn(parent); @@ -476,7 +477,7 @@ public class DynamicProvisioningTest { return provisionedHost; }) .collect(Collectors.toList()); - }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any()); + }).when(hostProvisioner).provisionHosts(any(), any(), any(), any(), any(), any(), any()); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java index 18fcb56d87f..fdef4135c16 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/VirtualNodeProvisioningTest.java @@ -333,6 +333,34 @@ public class VirtualNodeProvisioningTest { assertNodeParentReservation(tester.getNodes(application1_1).asList(), Optional.empty(), tester); // Reservation is cleared after activation } + @Test + public void respects_exclusive_to_cluster_type() { + NodeResources resources = new NodeResources(10, 10, 100, 10); + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).build(); + + tester.makeReadyNodes(10, resources, Optional.empty(), NodeType.host, 1); + tester.activateTenantHosts(); + // All hosts are exclusive to content nodes + tester.patchNodes(tester.nodeRepository().nodes().list().asList(), node -> node.withExclusiveToClusterType(ClusterSpec.Type.content)); + + Version wantedVespaVersion = Version.fromString("6.39"); + try { + // No capacity for 'container' nodes + tester.prepare(applicationId, + ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("myContent")).vespaVersion(wantedVespaVersion).build(), + 6, 1, resources); + fail("Expected to fail due to out of capacity"); + } catch (OutOfCapacityException ignored) { } + + // Same cluster, but content type is now 'content' + List<HostSpec> nodes = tester.prepare(applicationId, + ClusterSpec.request(ClusterSpec.Type.content, ClusterSpec.Id.from("myContent")).vespaVersion(wantedVespaVersion).build(), + 6, 1, resources); + tester.activate(applicationId, nodes); + + assertEquals(6, tester.nodeRepository().nodes().list(Node.State.active).owner(applicationId).size()); + } + /** Exclusive app first, then non-exclusive: Should give the same result as below */ @Test public void application_deployment_with_exclusive_app_first() { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java index dd16d4674ad..6c052a6c364 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiTest.java @@ -993,11 +993,15 @@ public class NodesV2ApiTest { String url = "http://localhost:8080/nodes/v2/node/dockerhost1.yahoo.com"; tester.assertPartialResponse(new Request(url), "exclusiveTo", false); // Initially there is no exclusiveTo - assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": \"t1:a1:i1\"}"), Request.Method.PATCH), + assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveToApplicationId\": \"t1:a1:i1\"}"), Request.Method.PATCH), "{\"message\":\"Updated dockerhost1.yahoo.com\"}"); tester.assertPartialResponse(new Request(url), "exclusiveTo\":\"t1:a1:i1\",", true); - assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": null}"), Request.Method.PATCH), + assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveToClusterType\": \"admin\"}"), Request.Method.PATCH), + "{\"message\":\"Updated dockerhost1.yahoo.com\"}"); + tester.assertPartialResponse(new Request(url), "exclusiveTo\":\"t1:a1:i1\",\"exclusiveToClusterType\":\"admin\",", true); + + assertResponse(new Request(url, Utf8.toBytes("{\"exclusiveTo\": null, \"exclusiveToClusterType\": null}"), Request.Method.PATCH), "{\"message\":\"Updated dockerhost1.yahoo.com\"}"); tester.assertPartialResponse(new Request(url), "exclusiveTo", false); } |