summaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java
diff options
context:
space:
mode:
Diffstat (limited to 'node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java211
1 files changed, 120 insertions, 91 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java
index 0927ffe3ce2..ca4b2c99ef0 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java
@@ -6,7 +6,6 @@ import com.google.common.primitives.UnsignedBytes;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.CloudName;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.hosted.provision.LockedNodeList;
import com.yahoo.vespa.hosted.provision.Node;
@@ -17,7 +16,6 @@ import com.yahoo.vespa.hosted.provision.persistence.NameResolver.RecordType;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Comparator;
-import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
@@ -38,6 +36,43 @@ import static com.yahoo.config.provision.NodeType.proxyhost;
*/
public record IP() {
+ /** IP version. Can be compared with ==, !=, and equals(). */
+ public static class Version {
+ public static final Version v4 = new Version(4);
+ public static final Version v6 = new Version(6);
+
+ private final int version;
+
+ public static Version fromIpAddress(String ipAddress) {
+ if (ipAddress.contains(":")) return v6;
+ if (ipAddress.contains(".")) return v4;
+ throw new IllegalArgumentException("Failed to deduce the IP version from the textual representation of the IP address: " + ipAddress);
+ }
+
+ public static Version fromIsIpv6(boolean isIpv6) { return isIpv6 ? v6 : v4; }
+
+ private Version(int version) { this.version = version; }
+
+ public boolean is4() { return version == 4; }
+ public boolean is6() { return version == 6; }
+
+ public RecordType toForwardRecordType() { return is4() ? RecordType.A : RecordType.AAAA; }
+
+ @Override
+ public String toString() { return "IPv" + version; }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Version version1 = (Version) o;
+ return version == version1.version;
+ }
+
+ @Override
+ public int hashCode() { return Objects.hash(version); }
+ }
+
/** Comparator for sorting IP addresses by their natural order */
public static final Comparator<InetAddress> NATURAL_ORDER = (ip1, ip2) -> {
byte[] address1 = ip1.getAddress();
@@ -156,28 +191,28 @@ public record IP() {
}
/** A list of IP addresses and their protocol */
- record IpAddresses(Set<String> addresses, Protocol protocol) {
+ record IpAddresses(Set<String> addresses, Stack stack) {
- public IpAddresses(Set<String> addresses, Protocol protocol) {
+ public IpAddresses(Set<String> addresses, Stack stack) {
this.addresses = Collections.unmodifiableSet(new LinkedHashSet<>(Objects.requireNonNull(addresses, "addresses must be non-null")));
- this.protocol = Objects.requireNonNull(protocol, "type must be non-null");
+ this.stack = Objects.requireNonNull(stack, "type must be non-null");
}
/** Create addresses of the given set */
private static IpAddresses of(Set<String> addresses) {
long ipv6AddrCount = addresses.stream().filter(IP::isV6).count();
if (ipv6AddrCount == addresses.size()) { // IPv6-only
- return new IpAddresses(addresses, Protocol.ipv6);
+ return new IpAddresses(addresses, Stack.ipv6);
}
long ipv4AddrCount = addresses.stream().filter(IP::isV4).count();
if (ipv4AddrCount == addresses.size()) { // IPv4-only
- return new IpAddresses(addresses, Protocol.ipv4);
+ return new IpAddresses(addresses, Stack.ipv4);
}
// If we're dual-stacked, we must have an equal number of addresses of each protocol.
if (ipv4AddrCount == ipv6AddrCount) {
- return new IpAddresses(addresses, Protocol.dualStack);
+ return new IpAddresses(addresses, Stack.dual);
}
throw new IllegalArgumentException(String.format("Dual-stacked IP address list must have an " +
@@ -186,16 +221,21 @@ public record IP() {
ipv6AddrCount, ipv4AddrCount));
}
- public enum Protocol {
+ public enum Stack {
- dualStack("dual-stack"),
- ipv4("IPv4-only"),
- ipv6("IPv6-only");
+ dual("dual-stack", Version.v4, Version.v6),
+ ipv4("IPv4-only", Version.v4),
+ ipv6("IPv6-only", Version.v6);
private final String description;
+ private final Set<Version> versions;
- Protocol(String description) { this.description = description; }
+ Stack(String description, Version... versions) {
+ this.description = description;
+ this.versions = Set.of(versions);
+ }
+ public boolean supports(Version version) { return versions.contains(version); }
}
}
@@ -227,34 +267,34 @@ public record IP() {
/**
* Find a free allocation in this pool. Note that the allocation is not final until it is assigned to a node
*
- * @param nodes a locked list of all nodes in the repository
+ * @param context allocation context
+ * @param nodes a locked list of all nodes in the repository
* @return an allocation from the pool, if any can be made
*/
- public Optional<Allocation> findAllocation(LockedNodeList nodes, NameResolver resolver, boolean hasPtr) {
+ public Optional<Allocation> findAllocation(Allocation.Context context, LockedNodeList nodes) {
if (ipAddresses.addresses.isEmpty()) {
// IP addresses have not yet been resolved and should be done later.
return findUnusedHostnames(nodes).map(Allocation::ofHostname)
.findFirst();
}
- if (!hasPtr) {
- // Without PTR records (reverse IP mapping): Ensure only forward resolving from hostnames.
- return findUnusedHostnames(nodes).findFirst().map(hostname -> Allocation.fromHostname(hostname, resolver, ipAddresses.protocol));
- }
+ Set<String> unusedIps = findUnusedIpAddresses(nodes);
- if (ipAddresses.protocol == IpAddresses.Protocol.ipv4) {
- return findUnusedIpAddresses(nodes).stream()
- .findFirst()
- .map(addr -> Allocation.ofIpv4(addr, resolver));
+ if (context.allocateFromUnusedHostname())
+ return findUnusedHostnames(nodes).findFirst().map(hostname -> Allocation.fromHostname(context, hostname, ipAddresses.stack, unusedIps));
+
+ if (ipAddresses.stack == IpAddresses.Stack.ipv4) {
+ return unusedIps.stream()
+ .findFirst()
+ .map(addr -> Allocation.ofIpv4(addr, context.resolver()));
}
- var unusedAddresses = findUnusedIpAddresses(nodes);
- var allocation = unusedAddresses.stream()
- .filter(IP::isV6)
- .findFirst()
- .map(addr -> Allocation.ofIpv6(addr, resolver));
+ var allocation = unusedIps.stream()
+ .filter(IP::isV6)
+ .findFirst()
+ .map(addr -> Allocation.ofIpv6(addr, context.resolver()));
allocation.flatMap(Allocation::ipv4Address).ifPresent(ipv4Address -> {
- if (!unusedAddresses.contains(ipv4Address)) {
+ if (!unusedIps.contains(ipv4Address)) {
throw new IllegalArgumentException("Allocation resolved " + ipv4Address + " from hostname " +
allocation.get().hostname +
", but that address is not owned by this node");
@@ -299,6 +339,36 @@ public record IP() {
Objects.requireNonNull(ipv6Address, "ipv6Address must be non-null");
}
+ public static class Context {
+ private final CloudName cloudName;
+ private final boolean exclave;
+ private final NameResolver resolver;
+
+ private Context(CloudName cloudName, boolean exclave, NameResolver resolver) {
+ this.cloudName = cloudName;
+ this.exclave = exclave;
+ this.resolver = resolver;
+ }
+
+ public static Context from(CloudName cloudName, boolean exclave, NameResolver resolver) {
+ return new Context(cloudName, exclave, resolver);
+ }
+
+ public NameResolver resolver() { return resolver; }
+
+ public boolean allocateFromUnusedHostname() { return exclave; }
+
+ public boolean hasIpNotInDns(Version version) {
+ if (exclave && cloudName == CloudName.GCP && version.is4()) {
+ // Exclave nodes in GCP have IPv4, because load balancers backends are required to be IPv4,
+ // but it's private (10.x). The hostname only resolves to the public IPv6 address.
+ return true;
+ }
+
+ return false;
+ }
+ }
+
/**
* Allocate an IPv6 address.
*
@@ -353,22 +423,30 @@ public record IP() {
return new Allocation(hostname4, Optional.of(addresses.get(0)), Optional.empty());
}
- private static Allocation fromHostname(HostName hostname, NameResolver resolver, IpAddresses.Protocol protocol) {
- // Resolve both A and AAAA to verify they match the protocol and to avoid surprises later on.
+ private static Allocation fromHostname(Context context, HostName hostname, IpAddresses.Stack stack, Set<String> unusedIps) {
+ Optional<String> ipv4Address = resolveAndVerify(context, hostname, stack, IP.Version.v4, unusedIps);
+ Optional<String> ipv6Address = resolveAndVerify(context, hostname, stack, IP.Version.v6, unusedIps);
+ return new Allocation(hostname.value(), ipv4Address, ipv6Address);
+ }
- Optional<String> ipv4Address = resolveOptional(hostname.value(), resolver, RecordType.A);
- if (protocol != IpAddresses.Protocol.ipv6 && ipv4Address.isEmpty())
- throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " did not resolve to an IPv4 address");
- if (protocol == IpAddresses.Protocol.ipv6 && ipv4Address.isPresent())
- throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " has an IPv4 address: " + ipv4Address.get());
+ private static Optional<String> resolveAndVerify(Context context, HostName hostname, IpAddresses.Stack stack, Version version, Set<String> unusedIps) {
+ if (context.hasIpNotInDns(version)) {
+ List<String> candidates = unusedIps.stream()
+ .filter(a -> IP.Version.fromIpAddress(a).equals(version))
+ .toList();
+ if (candidates.size() != 1) {
+ throw new IllegalStateException("Unable to find a unique child IP address of " + hostname + ": Found " + candidates);
+ }
+ return candidates.stream().findFirst();
+ }
- Optional<String> ipv6Address = resolveOptional(hostname.value(), resolver, RecordType.AAAA);
- if (protocol != IpAddresses.Protocol.ipv4 && ipv6Address.isEmpty())
- throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " did not resolve to an IPv6 address");
- if (protocol == IpAddresses.Protocol.ipv4 && ipv6Address.isPresent())
- throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " has an IPv6 address: " + ipv6Address.get());
+ Optional<String> address = resolveOptional(hostname.value(), context.resolver(), version.toForwardRecordType());
+ if (stack.supports(version) && address.isEmpty())
+ throw new IllegalArgumentException(stack.description + " hostname " + hostname.value() + " did not resolve to an " + version + " address");
+ if (!stack.supports(version) && address.isPresent())
+ throw new IllegalArgumentException(stack.description + " hostname " + hostname.value() + " has an " + version + " address: " + address.get());
- return new Allocation(hostname.value(), ipv4Address, ipv6Address);
+ return address;
}
private static Optional<String> resolveOptional(String hostname, NameResolver resolver, RecordType recordType) {
@@ -401,55 +479,6 @@ public record IP() {
}
}
- public enum DnsRecordType { FORWARD, PUBLIC_FORWARD, REVERSE }
-
- /** Returns the set of DNS record types for a host and its children and the given version (ipv6), host type, etc. */
- public static Set<DnsRecordType> dnsRecordTypesFor(boolean ipv6, NodeType hostType, CloudName cloudName, boolean exclave) {
- if (cloudName == CloudName.AWS)
- return exclave ?
- EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD) :
- EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD, DnsRecordType.REVERSE);
-
- if (cloudName == CloudName.GCP) {
- if (exclave) {
- return ipv6 ?
- EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD) :
- EnumSet.noneOf(DnsRecordType.class);
- } else {
- return hostType == confighost && ipv6 ?
- EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.REVERSE, DnsRecordType.PUBLIC_FORWARD) :
- EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.REVERSE);
- }
- }
-
- throw new IllegalArgumentException("Does not manage DNS for cloud " + cloudName);
- }
-
- /** Verify DNS configuration of given hostname and IP address */
- public static void verifyDns(String hostname, String ipAddress, NodeType nodeType, NameResolver resolver,
- CloudAccount cloudAccount, Zone zone) {
- boolean ipv6 = isV6(ipAddress);
- Set<DnsRecordType> recordTypes = dnsRecordTypesFor(ipv6, nodeType, zone.cloud().name(), cloudAccount.isExclave(zone));
-
- if (recordTypes.contains(DnsRecordType.FORWARD)) {
- RecordType recordType = ipv6 ? RecordType.AAAA : RecordType.A;
- Set<String> addresses = resolver.resolve(hostname, recordType);
- if (!addresses.equals(Set.of(ipAddress)))
- throw new IllegalArgumentException("Expected " + hostname + " to resolve to " + ipAddress +
- ", but got " + addresses);
- }
-
- if (recordTypes.contains(DnsRecordType.REVERSE)) {
- Optional<String> reverseHostname = resolver.resolveHostname(ipAddress);
- if (reverseHostname.isEmpty())
- throw new IllegalArgumentException(ipAddress + " did not resolve to a hostname");
-
- if (!reverseHostname.get().equals(hostname))
- throw new IllegalArgumentException(ipAddress + " resolved to " + reverseHostname.get() +
- ", which does not match expected hostname " + hostname);
- }
- }
-
/** Convert IP address to string. This uses :: for zero compression in IPv6 addresses. */
public static String asString(InetAddress inetAddress) {
return InetAddresses.toAddrString(inetAddress);