All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.vespa.hosted.provision.node.IP Maven / Gradle / Ivy

There is a newer version: 8.458.13
Show newest version
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.node;

import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import com.google.common.primitives.UnsignedBytes;
import com.yahoo.vespa.hosted.provision.LockedNodeList;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeList;
import com.yahoo.vespa.hosted.provision.persistence.NameResolver;
import com.yahoo.vespa.hosted.provision.persistence.NameResolver.RecordType;

import java.net.InetAddress;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.yahoo.config.provision.NodeType.confighost;
import static com.yahoo.config.provision.NodeType.controllerhost;
import static com.yahoo.config.provision.NodeType.proxyhost;

/**
 * This handles IP address configuration and allocation.
 *
 * @author mpolden
 */
public class IP {

    /** Comparator for sorting IP addresses by their natural order */
    public static final Comparator NATURAL_ORDER = (ip1, ip2) -> {
        byte[] address1 = ip1.getAddress();
        byte[] address2 = ip2.getAddress();

        // IPv4 always sorts before IPv6
        if (address1.length < address2.length) return -1;
        if (address1.length > address2.length) return 1;

        // Compare each octet
        for (int i = 0; i < address1.length; i++) {
            int b1 = UnsignedBytes.toInt(address1[i]);
            int b2 = UnsignedBytes.toInt(address2[i]);
            if (b1 == b2) {
                continue;
            }
            if (b1 < b2) {
                return -1;
            } else {
                return 1;
            }
        }
        return 0;
    };

    /** IP configuration of a node */
    public static class Config {

        public static final Config EMPTY = Config.ofEmptyPool(Set.of());

        private final Set primary;
        private final Pool pool;

        public static Config ofEmptyPool(Set primary) {
            return new Config(primary, Set.of(), List.of());
        }

        public static Config of(Set primary, Set ipPool, List
addressPool) { return new Config(primary, ipPool, addressPool); } /** LEGACY TEST CONSTRUCTOR - use of() variants and/or the with- methods. */ public Config(Set primary, Set pool) { this(primary, pool, List.of()); } /** DO NOT USE: Public for NodeSerializer. */ public Config(Set primary, Set pool, List
addresses) { this.primary = ImmutableSet.copyOf(Objects.requireNonNull(primary, "primary must be non-null")); this.pool = Pool.of(Objects.requireNonNull(pool, "pool must be non-null"), Objects.requireNonNull(addresses, "addresses must be non-null")); } /** The primary addresses of this. These addresses are used when communicating with the node itself */ public Set primary() { return primary; } /** Returns the IP address pool available on a node */ public Pool pool() { return pool; } /** Returns a copy of this with pool set to given value */ public Config withPool(Pool pool) { return new Config(primary, pool.ipSet(), pool.getAddressList()); } /** Returns a copy of this with pool set to given value */ public Config withPrimary(Set primary) { return new Config(primary, pool.ipSet(), pool.getAddressList()); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Config config = (Config) o; return primary.equals(config.primary) && pool.equals(config.pool); } @Override public int hashCode() { return Objects.hash(primary, pool); } @Override public String toString() { return String.format("ip config primary=%s pool=%s", primary, pool.ipSet()); } /** * Verify IP config of given nodes * * @throws IllegalArgumentException if there are IP conflicts with existing nodes */ public static List verify(List nodes, LockedNodeList allNodes) { for (var node : nodes) { for (var other : allNodes) { if (node.equals(other)) continue; if (canAssignIpOf(other, node)) continue; var addresses = new HashSet<>(node.ipConfig().primary()); var otherAddresses = new HashSet<>(other.ipConfig().primary()); if (node.type().isHost()) { // Addresses of a host can never overlap with any other nodes addresses.addAll(node.ipConfig().pool().ipSet()); otherAddresses.addAll(other.ipConfig().pool().ipSet()); } otherAddresses.retainAll(addresses); if (!otherAddresses.isEmpty()) throw new IllegalArgumentException("Cannot assign " + addresses + " to " + node.hostname() + ": " + otherAddresses + " already assigned to " + other.hostname()); } } return nodes; } /** Returns whether IP address of existing node can be assigned to node */ private static boolean canAssignIpOf(Node existingNode, Node node) { if (node.parentHostname().isPresent() == existingNode.parentHostname().isPresent()) return false; // Not a parent-child node if (node.parentHostname().isEmpty()) return canAssignIpOf(node, existingNode); if (!node.parentHostname().get().equals(existingNode.hostname())) return false; // Wrong host switch (node.type()) { case proxy: return existingNode.type() == proxyhost; case config: return existingNode.type() == confighost; case controller: return existingNode.type() == controllerhost; } return false; } public static Node verify(Node node, LockedNodeList allNodes) { return verify(List.of(node), allNodes).get(0); } } /** A list of IP addresses and their protocol */ public static class IpAddresses { private final Set ipAddresses; private final Protocol protocol; private IpAddresses(Set ipAddresses, Protocol protocol) { this.ipAddresses = ImmutableSet.copyOf(Objects.requireNonNull(ipAddresses, "addresses must be non-null")); this.protocol = Objects.requireNonNull(protocol, "type must be non-null"); } public Set asSet() { return ipAddresses; } /** The protocol of addresses in this */ public Protocol protocol() { return protocol; } /** Create addresses of the given set */ private static IpAddresses of(Set addresses) { long ipv6AddrCount = addresses.stream().filter(IP::isV6).count(); if (ipv6AddrCount == addresses.size()) { // IPv6-only return new IpAddresses(addresses, Protocol.ipv6); } long ipv4AddrCount = addresses.stream().filter(IP::isV4).count(); if (ipv4AddrCount == addresses.size()) { // IPv4-only return new IpAddresses(addresses, Protocol.ipv4); } // If we're dual-stacked, we must must have an equal number of addresses of each protocol. if (ipv4AddrCount == ipv6AddrCount) { return new IpAddresses(addresses, Protocol.dualStack); } throw new IllegalArgumentException(String.format("Dual-stacked IP address list must have an " + "equal number of addresses of each version " + "[IPv6 address count = %d, IPv4 address count = %d]", ipv6AddrCount, ipv4AddrCount)); } public enum Protocol { dualStack, ipv4, ipv6 } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; IpAddresses that = (IpAddresses) o; return ipAddresses.equals(that.ipAddresses) && protocol == that.protocol; } @Override public int hashCode() { return Objects.hash(ipAddresses, protocol); } } /** * A pool of addresses from which an allocation can be made. * * Addresses in this are available for use by Linux containers. */ public static class Pool { private final IpAddresses ipAddresses; private final List
addresses; /** Creates an empty pool. */ public static Pool of() { return of(Set.of(), List.of()); } /** Create a new pool containing given ipAddresses */ public static Pool of(Set ipAddresses, List
addresses) { IpAddresses ips = IpAddresses.of(ipAddresses); return new Pool(ips, addresses); } private Pool(IpAddresses ipAddresses, List
addresses) { this.ipAddresses = Objects.requireNonNull(ipAddresses, "ipAddresses must be non-null"); this.addresses = Objects.requireNonNull(addresses, "addresses must be non-null"); } /** * 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 * @return an allocation from the pool, if any can be made */ public Optional findAllocation(LockedNodeList nodes, NameResolver resolver) { if (ipAddresses.asSet().isEmpty()) { // IP addresses have not yet been resolved and should be done later. return findUnusedAddressStream(nodes) .map(Allocation::ofAddress) .findFirst(); } if (ipAddresses.protocol == IpAddresses.Protocol.ipv4) { return findUnusedIpAddresses(nodes).stream() .findFirst() .map(addr -> Allocation.ofIpv4(addr, resolver)); } var unusedAddresses = findUnusedIpAddresses(nodes); var allocation = unusedAddresses.stream() .filter(IP::isV6) .findFirst() .map(addr -> Allocation.ofIpv6(addr, resolver)); allocation.flatMap(Allocation::ipv4Address).ifPresent(ipv4Address -> { if (!unusedAddresses.contains(ipv4Address)) { throw new IllegalArgumentException("Allocation resolved " + ipv4Address + " from hostname " + allocation.get().hostname + ", but that address is not owned by this node"); } }); return allocation; } /** * Finds all unused IP addresses in this pool * * @param nodes a list of all nodes in the repository */ public Set findUnusedIpAddresses(NodeList nodes) { var unusedAddresses = new LinkedHashSet<>(ipSet()); nodes.matching(node -> node.ipConfig().primary().stream().anyMatch(ip -> ipSet().contains(ip))) .forEach(node -> unusedAddresses.removeAll(node.ipConfig().primary())); return Collections.unmodifiableSet(unusedAddresses); } private Stream
findUnusedAddressStream(NodeList nodes) { Set hostnames = nodes.stream().map(Node::hostname).collect(Collectors.toSet()); return addresses.stream().filter(address -> !hostnames.contains(address.hostname())); } public IpAddresses.Protocol getProtocol() { return ipAddresses.protocol; } /** Returns the IP addresses in this pool as a set */ public Set ipSet() { return ipAddresses.asSet(); } public List
getAddressList() { return addresses; } public Pool withIpAddresses(Set ipAddresses) { return Pool.of(ipAddresses, addresses); } public Pool withAddresses(List
addresses) { return Pool.of(ipAddresses.ipAddresses, addresses); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Pool pool = (Pool) o; return ipAddresses.equals(pool.ipAddresses) && addresses.equals(pool.addresses); } @Override public int hashCode() { return Objects.hash(ipAddresses, addresses); } } /** An address allocation from a pool */ public static class Allocation { private final String hostname; private final Optional ipv4Address; private final Optional ipv6Address; private Allocation(String hostname, Optional ipv4Address, Optional ipv6Address) { this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null"); this.ipv4Address = Objects.requireNonNull(ipv4Address, "ipv4Address must be non-null"); this.ipv6Address = Objects.requireNonNull(ipv6Address, "ipv6Address must be non-null"); } /** * Allocate an IPv6 address. * * A successful allocation is guaranteed to have an IPv6 address, but may also have an IPv4 address if the * hostname of the IPv6 address has an A record. * * @param ipv6Address Unassigned IPv6 address * @param resolver DNS name resolver to use * @throws IllegalArgumentException if DNS is misconfigured * @return An allocation containing 1 IPv6 address and 1 IPv4 address (if hostname is dual-stack) */ private static Allocation ofIpv6(String ipv6Address, NameResolver resolver) { if (!isV6(ipv6Address)) { throw new IllegalArgumentException("Invalid IPv6 address '" + ipv6Address + "'"); } String hostname6 = resolver.resolveHostname(ipv6Address).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + ipv6Address)); List ipv4Addresses = resolver.resolveAll(hostname6).stream() .filter(IP::isV4) .collect(Collectors.toList()); if (ipv4Addresses.size() > 1) { throw new IllegalArgumentException("Hostname " + hostname6 + " resolved to more than 1 IPv4 address: " + ipv4Addresses); } Optional ipv4Address = ipv4Addresses.stream().findFirst(); ipv4Address.ifPresent(addr -> { String hostname4 = resolver.resolveHostname(addr).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + addr)); if (!hostname6.equals(hostname4)) { throw new IllegalArgumentException(String.format("Hostnames resolved from each IP address do not " + "point to the same hostname [%s -> %s, %s -> %s]", ipv6Address, hostname6, addr, hostname4)); } }); return new Allocation(hostname6, ipv4Address, Optional.of(ipv6Address)); } /** * Allocate an IPv4 address. A successful allocation is guaranteed to have an IPv4 address. * * @param ipAddress Unassigned IPv4 address * @param resolver DNS name resolver to use * @return An allocation containing 1 IPv4 address. */ private static Allocation ofIpv4(String ipAddress, NameResolver resolver) { String hostname4 = resolver.resolveHostname(ipAddress).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + ipAddress)); List addresses = resolver.resolveAll(hostname4).stream() .filter(IP::isV4) .collect(Collectors.toList()); if (addresses.size() != 1) { throw new IllegalArgumentException("Hostname " + hostname4 + " did not resolve to exactly 1 address. " + "Resolved: " + addresses); } return new Allocation(hostname4, Optional.of(addresses.get(0)), Optional.empty()); } private static Allocation ofAddress(Address address) { return new Allocation(address.hostname(), Optional.empty(), Optional.empty()); } /** Hostname pointing to the IP addresses in this */ public String hostname() { return hostname; } /** IPv4 address of this allocation */ public Optional ipv4Address() { return ipv4Address; } /** IPv6 address of this allocation */ public Optional ipv6Address() { return ipv6Address; } /** All IP addresses in this */ public Set addresses() { ImmutableSet.Builder builder = ImmutableSet.builder(); ipv4Address.ifPresent(builder::add); ipv6Address.ifPresent(builder::add); return builder.build(); } @Override public String toString() { return String.format("Address allocation [hostname=%s, IPv4=%s, IPv6=%s]", hostname, ipv4Address.orElse(""), ipv6Address.orElse("")); } } /** Parse given IP address string */ public static InetAddress parse(String ipAddress) { try { return InetAddresses.forString(ipAddress); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid IP address '" + ipAddress + "'", e); } } /** Verify DNS configuration of given hostname and IP address */ public static void verifyDns(String hostname, String ipAddress, NameResolver resolver) { RecordType recordType = isV6(ipAddress) ? RecordType.AAAA : RecordType.A; Set addresses = resolver.resolve(hostname, recordType); if (!addresses.equals(Set.of(ipAddress))) throw new IllegalArgumentException("Expected " + hostname + " to resolve to " + ipAddress + ", but got " + addresses); Optional 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); } /** Returns whether given string is an IPv4 address */ public static boolean isV4(String ipAddress) { return ipAddress.contains("."); } /** Returns whether given string is an IPv6 address */ public static boolean isV6(String ipAddress) { return ipAddress.contains(":"); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy