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 2018 Yahoo Holdings. 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.persistence.NameResolver;

import java.net.Inet4Address;
import java.net.Inet6Address;
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 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 = InetAddresses.forString(ip1).getAddress();
        byte[] address2 = InetAddresses.forString(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 = new Config(Set.of(), Set.of());

        private final Set primary;
        private final Pool pool;

        /** DO NOT USE in non-test code. Public for serialization purposes. */
        public Config(Set primary, Set pool) {
            this.primary = ImmutableSet.copyOf(Objects.requireNonNull(primary, "primary must be non-null"));
            this.pool = new Pool(Objects.requireNonNull(pool, "pool 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 with(Pool pool) {
            return new Config(primary, pool.asSet());
        }

        /** Returns a copy of this with pool set to given value */
        public Config with(Set primary) {
            return new Config(require(primary), pool.asSet());
        }

        @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.asSet());
        }

        /** Validates and returns the given addresses */
        public static Set require(Set addresses) {
            try {
                addresses.forEach(InetAddresses::forString);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("Found one or more invalid addresses in " + addresses, e);
            }
            return addresses;
        }

        /**
         * 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().isDockerHost()) { // Addresses of a host can never overlap with any other nodes
                        addresses.addAll(node.ipConfig().pool().asSet());
                        otherAddresses.addAll(other.ipConfig().pool().asSet());
                    }
                    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 pool of IP addresses. Addresses in this are destined for use by Docker containers */
    public static class Pool {

        private final Set addresses;

        private Pool(Set addresses) {
            this.addresses = ImmutableSet.copyOf(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) {
            var unusedAddresses = findUnused(nodes);
            var allocation = unusedAddresses.stream()
                                            .filter(IP::isV6)
                                            .findFirst()
                                            .map(addr -> Allocation.resolveFrom(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 addresses in this pool
         *
         * @param nodes Locked list of all nodes in the repository
         */
        public Set findUnused(LockedNodeList nodes) {
            var unusedAddresses = new LinkedHashSet<>(addresses);
            nodes.filter(node -> node.ipConfig().primary().stream().anyMatch(addresses::contains))
                 .forEach(node -> unusedAddresses.removeAll(node.ipConfig().primary()));
            return Collections.unmodifiableSet(unusedAddresses);
        }

        public Set asSet() {
            return addresses;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Pool that = (Pool) o;
            return Objects.equals(addresses, that.addresses);
        }

        @Override
        public int hashCode() {
            return Objects.hash(addresses);
        }

        public static Pool of(Set pool) {
            return new Pool(require(pool));
        }

        /** Validates and returns the given IP address pool */
        public static Set require(Set pool) {
            long ipv6AddrCount = pool.stream().filter(IP::isV6).count();
            if (ipv6AddrCount == pool.size()) {
                return pool; // IPv6-only pool is valid
            }

            long ipv4AddrCount = pool.stream().filter(IP::isV4).count();
            if (ipv4AddrCount == ipv6AddrCount) {
                return pool;
            }

            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));
        }

    }

    /** An IP address allocation from a pool */
    public static class Allocation {

        private final String hostname;
        private final String ipv6Address;
        private final Optional ipv4Address;

        private Allocation(String hostname, String ipv6Address, Optional ipv4Address) {
            Objects.requireNonNull(ipv6Address, "ipv6Address must be non-null");
            if (!isV6(ipv6Address)) {
                throw new IllegalArgumentException("Invalid IPv6 address '" + ipv6Address + "'");
            }

            Objects.requireNonNull(ipv4Address, "ipv4Address must be non-null");
            if (ipv4Address.isPresent() && !isV4(ipv4Address.get())) {
                throw new IllegalArgumentException("Invalid IPv4 address '" + ipv4Address + "'");
            }
            this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null");
            this.ipv6Address = ipv6Address;
            this.ipv4Address = ipv4Address;
        }

        /**
         * Resolve the IP addresses and hostname of this allocation
         *
         * @param ipv6Address Unassigned IPv6 address
         * @param resolver DNS name resolver to use
         * @throws IllegalArgumentException if DNS is misconfigured
         * @return The allocation containing 1 IPv6 address and 1 IPv4 address (if hostname is dual-stack)
         */
        public static Allocation resolveFrom(String ipv6Address, NameResolver resolver) {
            String hostname6 = resolver.getHostname(ipv6Address).orElseThrow(() -> new IllegalArgumentException("Could not resolve IP address: " + ipv6Address));
            List ipv4Addresses = resolver.getAllByNameOrThrow(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.getHostname(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, ipv6Address, ipv4Address);
        }

        /** Hostname pointing to the IP addresses in this */
        public String hostname() {
            return hostname;
        }

        /** IPv6 address in this allocation */
        public String ipv6Address() {
            return ipv6Address;
        }

        /** IPv4 address in this allocation */
        public Optional ipv4Address() {
            return ipv4Address;
        }

        /** All IP addresses in this */
        public Set addresses() {
            ImmutableSet.Builder builder = ImmutableSet.builder();
            ipv4Address.ifPresent(builder::add);
            builder.add(ipv6Address);
            return builder.build();
        }

        @Override
        public String toString() {
            return "ipv6Address='" + ipv6Address + '\'' +
                   ", ipv4Address='" + ipv4Address.orElse("none") + '\'';
        }

    }

    /** Returns whether given string is an IPv4 address */
    public static boolean isV4(String ipAddress) {
        return InetAddresses.forString(ipAddress) instanceof Inet4Address;
    }

    /** Returns whether given string is an IPv6 address */
    public static boolean isV6(String ipAddress) {
        return InetAddresses.forString(ipAddress) instanceof Inet6Address;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy