com.yahoo.vespa.hosted.provision.testutils.MockHostProvisioner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of node-repository Show documentation
Show all versions of node-repository Show documentation
Keeps track of node assignment in a multi-application setup.
The newest version!
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.testutils;
import com.yahoo.component.Version;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.HostEvent;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.NodeAllocationException;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.vespa.hosted.provision.node.IP;
import com.yahoo.vespa.hosted.provision.provisioning.FatalProvisioningException;
import com.yahoo.vespa.hosted.provision.provisioning.HostIpConfig;
import com.yahoo.vespa.hosted.provision.provisioning.HostProvisionRequest;
import com.yahoo.vespa.hosted.provision.provisioning.HostProvisioner;
import com.yahoo.vespa.hosted.provision.provisioning.HostResourcesCalculator;
import com.yahoo.vespa.hosted.provision.provisioning.ProvisionedHost;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.yahoo.config.provision.NodeType.host;
/**
* @author mpolden
*/
public class MockHostProvisioner implements HostProvisioner {
private final List provisionedHosts = new ArrayList<>();
private final List hostEvents = new ArrayList<>();
private final List flavors;
private final MockNameResolver nameResolver;
private final int memoryTaxGb;
private final Set rebuildsCompleted = new HashSet<>();
private final Map hostFlavors = new HashMap<>();
private final Set upgradableFlavors = new HashSet<>();
private final Map behaviours = new HashMap<>();
private final Set osVersions = new HashSet<>();
private int deprovisionedHosts = 0;
public MockHostProvisioner(List flavors, MockNameResolver nameResolver, int memoryTaxGb) {
this.flavors = List.copyOf(flavors);
this.nameResolver = nameResolver;
this.memoryTaxGb = memoryTaxGb;
}
public MockHostProvisioner(List flavors) {
this(flavors, 0);
}
public MockHostProvisioner(List flavors, int memoryTaxGb) {
this(flavors, new MockNameResolver().mockAnyLookup(), memoryTaxGb);
}
/** Returns whether given behaviour is active for this invocation */
private boolean behaviour(Behaviour behaviour) {
return behaviours.computeIfPresent(behaviour, (k, old) -> old == 0 ? null : --old) != null;
}
@Override
public Runnable provisionHosts(HostProvisionRequest request, Predicate realHostResourcesWithinLimits, Consumer> whenProvisioned) throws NodeAllocationException {
if (behaviour(Behaviour.failProvisionRequest)) throw new NodeAllocationException("No capacity for provision request", true);
Flavor hostFlavor = hostFlavors.get(request.clusterType().orElse(ClusterSpec.Type.content));
if (hostFlavor == null)
hostFlavor = flavors.stream()
.filter(f -> request.sharing().isExclusiveAllocation() ? compatible(f, request.resources())
: satisfies(f, request.resources()))
.filter(f -> realHostResourcesWithinLimits.test(f.resources()))
.findFirst()
.orElseThrow(() -> new NodeAllocationException("No host flavor matches " + request.resources(), true));
List hosts = new ArrayList<>();
for (int index : request.indices()) {
String hostHostname = request.type() == host ? "host" + index : request.type().name() + index;
hosts.add(new ProvisionedHost("id-of-" + request.type().name() + index,
hostHostname,
hostFlavor,
request.type(),
request.sharing() == HostSharing.provision ? Optional.of(request.owner()) : Optional.empty(),
request.sharing().isExclusiveAllocation() ? Optional.of(request.owner()) : Optional.empty(),
Optional.empty(),
createHostnames(request.type(), hostFlavor, index),
request.resources(),
request.osVersion(),
request.cloudAccount()));
}
provisionedHosts.addAll(hosts);
whenProvisioned.accept(hosts);
return () -> {};
}
@Override
public HostIpConfig provision(Node host) throws FatalProvisioningException {
if (behaviour(Behaviour.failProvisioning)) throw new FatalProvisioningException("Failed to provision node(s)");
if (host.state() != Node.State.provisioned) throw new IllegalStateException("Host to provision must be in " + Node.State.provisioned);
Map result = new HashMap<>();
result.put(host.hostname(), createIpConfig(host));
host.ipConfig().pool().hostnames().forEach(hostname ->
result.put(hostname.value(), IP.Config.ofEmptyPool(List.copyOf(nameResolver.resolveAll(hostname.value())))));
return new HostIpConfig(result, Optional.empty());
}
@Override
public boolean deprovision(Node host) {
if (behaviour(Behaviour.failDeprovisioning)) throw new FatalProvisioningException("Failed to deprovision node");
provisionedHosts.removeIf(provisionedHost -> provisionedHost.hostHostname().equals(host.hostname()));
deprovisionedHosts++;
return true;
}
@Override
public RebuildResult replaceRootDisk(Collection hosts) {
List updated = new ArrayList<>();
Map failed = new LinkedHashMap<>();
for (Node host : hosts) {
if ( ! host.type().isHost()) failed.put(host, new IllegalArgumentException(host + " is not a host"));
if (rebuildsCompleted.remove(host.hostname())) {
updated.add(host.withWantToRetire(host.status().wantToRetire(), host.status().wantToDeprovision(),
false, false, Agent.system, Instant.ofEpochMilli(123)));
}
}
return new RebuildResult(updated, failed);
}
@Override
public List hostEventsIn(List cloudAccounts) {
return Collections.unmodifiableList(hostEvents);
}
@Override
public boolean canUpgradeFlavor(Node host, Node child, Predicate realHostResourcesWithinLimits) {
return upgradableFlavors.contains(host.flavor().name());
}
@Override
public Set osVersions(Node host, int majorVersion) {
return osVersions.stream().filter(v -> v.getMajor() == majorVersion).collect(Collectors.toUnmodifiableSet());
}
/** Returns the hosts that have been provisioned by this */
public List provisionedHosts() {
return Collections.unmodifiableList(provisionedHosts);
}
/** Returns the number of hosts deprovisioned by this */
public int deprovisionedHosts() {
return deprovisionedHosts;
}
public MockHostProvisioner with(Behaviour first, Behaviour... rest) {
behaviours.put(first, Integer.MAX_VALUE);
for (var b : rest) {
behaviours.put(b, Integer.MAX_VALUE);
}
return this;
}
public MockHostProvisioner with(Behaviour behaviour, int count) {
behaviours.put(behaviour, count);
return this;
}
public MockHostProvisioner without(Behaviour first, Behaviour... rest) {
behaviours.remove(first);
for (var b : rest) {
behaviours.remove(b);
}
return this;
}
public MockHostProvisioner completeRebuildOf(String hostname) {
rebuildsCompleted.add(hostname);
return this;
}
public MockHostProvisioner setHostFlavor(String flavorName, ClusterSpec.Type ... types) {
Flavor flavor = flavors.stream().filter(f -> f.name().equals(flavorName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No such flavor '" + flavorName + "'"));
if (types.length == 0)
types = ClusterSpec.Type.values();
for (var type : types)
hostFlavors.put(type, flavor);
return this;
}
public MockHostProvisioner addUpgradableFlavor(String name) {
upgradableFlavors.add(name);
return this;
}
/** Sets the host flavor to use to the flavor matching these resources exactly, if any. */
public MockHostProvisioner setHostFlavorIfAvailable(NodeResources flavorAdvertisedResources, HostResourcesCalculator calculator, ClusterSpec.Type ... types) {
Optional hostFlavor = flavors.stream().filter(f -> calculator.advertisedResourcesOf(f).compatibleWith(flavorAdvertisedResources))
.findFirst();
if (types.length == 0)
types = ClusterSpec.Type.values();
for (var type : types)
hostFlavor.ifPresent(f -> hostFlavors.put(type, f));
return this;
}
public MockHostProvisioner addEvent(HostEvent event) {
hostEvents.add(event);
return this;
}
public MockHostProvisioner addOsVersion(Version version) {
osVersions.add(version);
return this;
}
public boolean compatible(Flavor flavor, NodeResources resources) {
NodeResources resourcesToVerify = resources.withMemoryGiB(resources.memoryGiB() - memoryTaxGb);
if (flavor.resources().storageType() == NodeResources.StorageType.remote
&& flavor.resources().diskGb() >= resources.diskGb())
resourcesToVerify = resourcesToVerify.withDiskGb(flavor.resources().diskGb());
if (flavor.resources().bandwidthGbps() >= resources.bandwidthGbps())
resourcesToVerify = resourcesToVerify.withBandwidthGbps(flavor.resources().bandwidthGbps());
return flavor.resources().compatibleWith(resourcesToVerify);
}
public boolean satisfies(Flavor flavor, NodeResources resources) {
return flavor.resources().satisfies(resources);
}
private List createHostnames(NodeType hostType, Flavor flavor, int hostIndex) {
long numAddresses = Math.max(2, Math.round(flavor.resources().bandwidthGbps()));
return IntStream.range(1, (int) numAddresses)
.mapToObj(i -> {
String hostname = hostType == host
? "host" + hostIndex + "-" + i
: hostType.childNodeType().name() + i;
return HostName.of(hostname);
})
.toList();
}
public IP.Config createIpConfig(Node node) {
if (!node.type().isHost()) throw new IllegalArgumentException("Node " + node + " is not a host");
int hostIndex = Integer.parseInt(node.hostname().replaceAll("^[a-z]+|-\\d+$", ""));
var addresses = List.of("::" + hostIndex + ":0");
var ipAddressPool = new ArrayList();
if (!behaviour(Behaviour.failDnsUpdate)) {
nameResolver.addRecord(node.hostname(), addresses.iterator().next());
int i = 1;
for (HostName hostName : node.ipConfig().pool().hostnames()) {
String ip = "::" + hostIndex + ":" + i++;
ipAddressPool.add(ip);
nameResolver.addRecord(hostName.value(), ip);
}
}
IP.Pool pool = node.ipConfig().pool().withIpAddresses(ipAddressPool);
return node.ipConfig().withPrimary(addresses).withPool(pool);
}
public enum Behaviour {
/** Fail call to {@link MockHostProvisioner#provision(com.yahoo.vespa.hosted.provision.Node)} */
failProvisioning,
/** Fail call to {@link MockHostProvisioner#provisionHosts(HostProvisionRequest, Predicate, Consumer)} */
failProvisionRequest,
/** Fail call to {@link MockHostProvisioner#deprovision(com.yahoo.vespa.hosted.provision.Node)} */
failDeprovisioning,
/** Fail DNS updates of provisioned hosts */
failDnsUpdate,
}
}