private Result canSetStateDownPermanently(NodeInfo nodeInfo, ClusterState clusterState, String newDescription) {
        var result = checkIfStateSetWithDifferentDescription(nodeInfo, newDescription);
        if (result.notAllowed())
            return result;

        State reportedState = nodeInfo.getReportedState().getState();
        if (reportedState != UP)
            return disallow("Reported state (" + reportedState + ") is not UP, so no bucket data is available");

        State currentState = clusterState.getNodeState(nodeInfo.getNode()).getState();
        if (currentState != RETIRED)
            return disallow("Only retired nodes are allowed to be set to DOWN in safe mode - is " + currentState);

        HostInfo hostInfo = nodeInfo.getHostInfo();
        Integer hostInfoNodeVersion = hostInfo.getClusterStateVersionOrNull();
        int clusterControllerVersion = clusterState.getVersion();
        int nodeIndex = nodeInfo.getNodeIndex();
        if (hostInfoNodeVersion == null || hostInfoNodeVersion != clusterControllerVersion)
            return disallow("Cluster controller at version " + clusterControllerVersion +
                            " got info for storage node " + nodeIndex + " at a different version " +
                            hostInfoNodeVersion);

        var metrics = dataMetricsFromHostInfo(hostInfo);
        // Bucket count metric should always be present
        if (metrics.buckets.isEmpty() || metrics.buckets.get().getLast() == null) {
            return disallow("Missing last value of the " + BUCKETS_METRIC_NAME + " metric for storage node " + nodeIndex);
        }

        // TODO should also ideally check merge pending from the distributors' perspectives.
        //  - This goes in particular for the global space, as we only check for zero entries in
        //    the _default_ space (as global entries are retained even for retired nodes).
        //  - Due to global merges being prioritized above everything else, it is highly unlikely
        //    that there will be any pending global merges, but the possibility still exists.
        //  - Would need wiring of aggregated content cluster stats
        var entriesCheckResult = checkZeroEntriesStoredOnContentNode(metrics, nodeIndex);
        return entriesCheckResult.orElseGet(() -> checkLegacyZeroBucketsStoredOnContentNode(metrics.buckets.get().getLast()));
    }
Checks if a node can be upgraded.

@author Haakon Dybdahl
clustercontroller-core/src/main/java/com/yahoo/vespa/clustercontroller/core/NodeStateChangeChecker.java