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

com.yahoo.vespa.hosted.provision.restapi.v2.NodesResponse Maven / Gradle / Ivy

There is a newer version: 8.465.15
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.restapi.v2;

import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.NodeType;
import com.yahoo.config.provision.serialization.NetworkPortsSerializer;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.JsonFormat;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.applicationmodel.HostName;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.node.Agent;
import com.yahoo.vespa.hosted.provision.node.History;
import com.yahoo.vespa.hosted.provision.node.filter.NodeFilter;
import com.yahoo.vespa.hosted.provision.os.OsVersion;
import com.yahoo.vespa.orchestrator.Orchestrator;
import com.yahoo.vespa.orchestrator.status.HostStatus;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

/**
* @author bratseth
*/
class NodesResponse extends HttpResponse {

    /** The responses this can create */
    public enum ResponseType { nodeList, stateList, nodesInStateList, singleNode }

    /** The request url minus parameters, with a trailing slash added if missing */
    private final String parentUrl;

    /** The parent url of nodes */
    private final String nodeParentUrl;

    private final NodeFilter filter;
    private final boolean recursive;
    private final Function> orchestrator;
    private final NodeRepository nodeRepository;
    private final Slime slime;
    private final NodeSerializer serializer = new NodeSerializer();

    public NodesResponse(ResponseType responseType, HttpRequest request,  
                         Orchestrator orchestrator, NodeRepository nodeRepository) {
        super(200);
        this.parentUrl = toParentUrl(request);
        this.nodeParentUrl = toNodeParentUrl(request);
        filter = NodesApiHandler.toNodeFilter(request);
        this.recursive = request.getBooleanProperty("recursive");
        this.orchestrator = orchestrator.getNodeStatuses();
        this.nodeRepository = nodeRepository;

        slime = new Slime();
        Cursor root = slime.setObject();
        switch (responseType) {
            case nodeList: nodesToSlime(root); break;
            case stateList : statesToSlime(root); break;
            case nodesInStateList: nodesToSlime(serializer.stateFrom(lastElement(parentUrl)), root); break;
            case singleNode : nodeToSlime(lastElement(parentUrl), root); break;
            default: throw new IllegalArgumentException();
        }
    }

    private String toParentUrl(HttpRequest request) {
        URI uri = request.getUri();
        String parentUrl = uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + uri.getPath();
        if ( ! parentUrl.endsWith("/"))
            parentUrl = parentUrl + "/";
        return parentUrl;
    }

    private String toNodeParentUrl(HttpRequest request) {
        URI uri = request.getUri();
        return uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + "/nodes/v2/node/";
    }

    @Override
    public void render(OutputStream stream) throws IOException {
        new JsonFormat(true).encode(stream, slime);
    }

    @Override
    public String getContentType() {
        return "application/json";
    }

    private void statesToSlime(Cursor root) {
        Cursor states = root.setObject("states");
        for (Node.State state : Node.State.values())
            toSlime(state, states.setObject(serializer.toString(state)));
    }

    private void toSlime(Node.State state, Cursor object) {
        object.setString("url", parentUrl + serializer.toString(state));
        if (recursive)
            nodesToSlime(state, object);
    }

    /** Outputs the nodes in the given state to a node array */
    private void nodesToSlime(Node.State state, Cursor parentObject) {
        Cursor nodeArray = parentObject.setArray("nodes");
        for (NodeType type : NodeType.values())
            toSlime(nodeRepository.getNodes(type, state), nodeArray);
    }

    /** Outputs all the nodes to a node array */
    private void nodesToSlime(Cursor parentObject) {
        Cursor nodeArray = parentObject.setArray("nodes");
        toSlime(nodeRepository.getNodes(), nodeArray);
    }

    private void toSlime(List nodes, Cursor array) {
        for (Node node : nodes) {
            if ( ! filter.matches(node)) continue;
            toSlime(node, recursive, array.addObject());
        }
    }

    private void nodeToSlime(String hostname, Cursor object) {
        Node node = nodeRepository.getNode(hostname).orElseThrow(() ->
                new NotFoundException("No node with hostname '" + hostname + "'"));
        toSlime(node, true, object);
    }

    private void toSlime(Node node, boolean allFields, Cursor object) {
        object.setString("url", nodeParentUrl + node.hostname());
        if ( ! allFields) return;
        object.setString("id", node.hostname());
        object.setString("state", serializer.toString(node.state()));
        object.setString("type", node.type().name());
        object.setString("hostname", node.hostname());
        object.setString("type", serializer.toString(node.type()));
        if (node.parentHostname().isPresent()) {
            object.setString("parentHostname", node.parentHostname().get());
        }
        object.setString("openStackId", node.id());
        object.setString("flavor", node.flavor().name());
        object.setString("canonicalFlavor", node.flavor().name());
        object.setDouble("minDiskAvailableGb", node.flavor().getMinDiskAvailableGb());
        object.setDouble("minMainMemoryAvailableGb", node.flavor().getMinMainMemoryAvailableGb());
        object.setDouble("minCpuCores", node.flavor().getMinCpuCores());
        if (node.flavor().cost() > 0)
            object.setLong("cost", node.flavor().cost());
        object.setBool("fastDisk", node.flavor().hasFastDisk());
        object.setDouble("bandwidthGbps", node.flavor().getBandwidthGbps());
        object.setString("environment", node.flavor().getType().name());
        node.allocation().ifPresent(allocation -> {
            toSlime(allocation.owner(), object.setObject("owner"));
            toSlime(allocation.membership(), object.setObject("membership"));
            object.setLong("restartGeneration", allocation.restartGeneration().wanted());
            object.setLong("currentRestartGeneration", allocation.restartGeneration().current());
            object.setString("wantedDockerImage", dockerImageFor(node.type()).withTag(allocation.membership().cluster().vespaVersion()).asString());
            object.setString("wantedVespaVersion", allocation.membership().cluster().vespaVersion().toFullString());
            allocation.networkPorts().ifPresent(ports -> NetworkPortsSerializer.toSlime(ports, object.setArray("networkPorts")));
            orchestrator.apply(new HostName(node.hostname()))
                        .map(status -> status == HostStatus.ALLOWED_TO_BE_DOWN)
                        .ifPresent(allowedToBeDown -> object.setBool("allowedToBeDown", allowedToBeDown));
        });
        object.setLong("rebootGeneration", node.status().reboot().wanted());
        object.setLong("currentRebootGeneration", node.status().reboot().current());
        node.status().osVersion().ifPresent(version -> object.setString("currentOsVersion", version.toFullString()));
        nodeRepository.osVersions().targetFor(node.type())
                      .filter(OsVersion::active) // Only include wantedOsVersion when active. When active is false, OS upgrades are paused.
                      .map(OsVersion::version)
                      .ifPresent(version -> object.setString("wantedOsVersion", version.toFullString()));
        node.status().firmwareVerifiedAt().ifPresent(instant -> object.setLong("currentFirmwareCheck", instant.toEpochMilli()));
        if (node.type().isDockerHost())
            nodeRepository.firmwareChecks().requiredAfter().ifPresent(after -> object.setLong("wantedFirmwareCheck", after.toEpochMilli()));
        node.status().vespaVersion().ifPresent(version -> object.setString("vespaVersion", version.toFullString()));
        currentDockerImage(node).ifPresent(dockerImage -> object.setString("currentDockerImage", dockerImage.asString()));
        object.setLong("failCount", node.status().failCount());
        object.setBool("wantToRetire", node.status().wantToRetire());
        object.setBool("wantToDeprovision", node.status().wantToDeprovision());
        toSlime(node.history(), object.setArray("history"));
        ipAddressesToSlime(node.ipAddresses(), object.setArray("ipAddresses"));
        ipAddressesToSlime(node.ipAddressPool().asSet(), object.setArray("additionalIpAddresses"));
        node.reports().toSlime(object, "reports");
        node.modelName().ifPresent(modelName -> object.setString("modelName", modelName));
    }

    private void toSlime(ApplicationId id, Cursor object) {
        object.setString("tenant", id.tenant().value());
        object.setString("application", id.application().value());
        object.setString("instance", id.instance().value());
    }

    private void toSlime(ClusterMembership membership, Cursor object) {
        object.setString("clustertype", membership.cluster().type().name());
        object.setString("clusterid", membership.cluster().id().value());
        object.setString("group", String.valueOf(membership.cluster().group().get().index()));
        object.setLong("index", membership.index());
        object.setBool("retired", membership.retired());
    }

    private void toSlime(History history, Cursor array) {
        for (History.Event event : history.events()) {
            Cursor object = array.addObject();
            object.setString("event", event.type().name());
            object.setLong("at", event.at().toEpochMilli());
            object.setString("agent", normalizedAgentUntilV6IsGone(event.agent()).name());
        }
    }

    // Hack: For non-docker noder, return current docker image as default prefix + current Vespa version
    // TODO: Remove current + wanted docker image from response for non-docker types
    private Optional currentDockerImage(Node node) {
        return node.status().dockerImage()
                .or(() -> Optional.of(node)
                        .filter(n -> n.flavor().getType() != Flavor.Type.DOCKER_CONTAINER)
                        .flatMap(n -> n.status().vespaVersion()
                                .map(version -> dockerImageFor(n.type()).withTag(version))));
    }

    // Docker hosts are not running in an image, but return the image of the node type running on it anyway,
    // this allows the docker host to pre-download the (likely) image its node will run
    private DockerImage dockerImageFor(NodeType nodeType) {
        return nodeRepository.dockerImage(nodeType.isDockerHost() ? nodeType.childNodeType() : nodeType);
    }


    /** maven-vespa-plugin @ v6 needs to deserialize nodes w/history. */
    private Agent normalizedAgentUntilV6IsGone(Agent agent) {
        return agent == Agent.NodeFailer ? Agent.system : agent;
    }

    private void ipAddressesToSlime(Set ipAddresses, Cursor array) {
        ipAddresses.forEach(array::addString);
    }

    private String lastElement(String path) {
        if (path.endsWith("/"))
            path = path.substring(0, path.length()-1);
        int lastSlash = path.lastIndexOf("/");
        if (lastSlash < 0) return path;
        return path.substring(lastSlash+1);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy