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

com.yahoo.vespa.hosted.controller.OsController Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;

import com.yahoo.component.Version;
import com.yahoo.config.provision.CloudName;
import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
import com.yahoo.vespa.hosted.controller.versions.OsVersion;
import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;

import java.time.Instant;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 *  A singleton owned by {@link Controller} which contains the methods and state for controlling OS upgrades.
 *
 * @author mpolden
 */
public record OsController(Controller controller) {

    private static final Logger LOG = Logger.getLogger(OsController.class.getName());

    public OsController {
        Objects.requireNonNull(controller);
    }

    /** Returns the target OS version for infrastructure in this system. The controller will drive infrastructure OS
     * upgrades to this version */
    public Optional target(CloudName cloud) {
        return targets().stream().filter(target -> target.osVersion().cloud().equals(cloud)).findFirst();
    }

    /** Returns all target OS versions in this system */
    public Set targets() {
        return curator().readOsVersionTargets();
    }

    /**
     * Set the target OS version for given cloud in this system.
     *
     * @param version The target OS version
     * @param cloud   The cloud to upgrade
     * @param force   Allow downgrades, and override pinned target (if any)
     * @param pin     Pin this version. This prevents automatic scheduling of upgrades until version is unpinned
     */
    public void upgradeTo(Version version, CloudName cloud, boolean force, boolean pin) {
        requireNonEmpty(version);
        requireCloud(cloud);
        Instant scheduledAt = controller.clock().instant();
        try (Mutex lock = curator().lockOsVersions()) {
            Map targets = curator().readOsVersionTargets().stream()
                                                               .collect(Collectors.toMap(t -> t.osVersion().cloud(),
                                                                                          Function.identity()));

            OsVersionTarget currentTarget = targets.get(cloud);
            boolean downgrade = false;
            if (currentTarget != null) {
                boolean versionChange = !currentTarget.osVersion().version().equals(version);
                downgrade = version.isBefore(currentTarget.osVersion().version());
                if (versionChange && currentTarget.pinned() && !force) {
                    throw new IllegalArgumentException("Cannot " + (downgrade ? "downgrade" : "upgrade") + " cloud " +
                                                       cloud.value() + "' to version " + version.toFullString() +
                                                       ": Current target is pinned. Add 'force' parameter to override");
                }
                if (downgrade && !force) {
                    throw new IllegalArgumentException("Cannot downgrade cloud '" + cloud.value() + "' to version " +
                                                       version.toFullString() + ": Missing 'force' parameter");
                }
                if (!versionChange && currentTarget.pinned() == pin) return; // No change
            }

            OsVersionTarget newTarget = new OsVersionTarget(new OsVersion(version, cloud), scheduledAt, pin, downgrade);
            targets.put(cloud, newTarget);
            curator().writeOsVersionTargets(new TreeSet<>(targets.values()));
            LOG.info("Triggered OS " + (downgrade ? "downgrade" : "upgrade") + " to " + version.toFullString() +
                     " in cloud " + cloud.value());
        }
    }

    /** Clear the target OS version for given cloud in this system */
    public void cancelUpgrade(CloudName cloudName) {
        try (Mutex lock = curator().lockOsVersions()) {
            Map targets = curator().readOsVersionTargets().stream()
                                                               .collect(Collectors.toMap(t -> t.osVersion().cloud(),
                                                                                          Function.identity()));
            if (targets.remove(cloudName) == null) {
                throw new IllegalArgumentException("Cloud '" + cloudName.value() + " has no OS upgrade target");
            }
            curator().writeOsVersionTargets(new TreeSet<>(targets.values()));
        }
    }

    /** Returns the current OS version status */
    public OsVersionStatus status() {
        return curator().readOsVersionStatus();
    }

    /** Replace the current OS version status with a new one */
    public void updateStatus(OsVersionStatus newStatus) {
        try (Mutex lock = curator().lockOsVersionStatus()) {
            OsVersionStatus currentStatus = curator().readOsVersionStatus();
            for (CloudName cloud : controller.clouds()) {
                Set newVersions = newStatus.versionsIn(cloud);
                if (currentStatus.versionsIn(cloud).size() > 1 && newVersions.size() == 1) {
                    LOG.info("All nodes in " + cloud + " cloud upgraded to OS version " +
                             newVersions.iterator().next().toFullString());
                }
            }
            curator().writeOsVersionStatus(newStatus);
        }
    }

    /** Certify an OS version as compatible with given Vespa version */
    public CertifiedOsVersion certify(Version version, CloudName cloud, Version vespaVersion) {
        requireNonEmpty(version);
        requireNonEmpty(vespaVersion);
        requireCloud(cloud);
        try (Mutex lock = curator().lockCertifiedOsVersions()) {
            OsVersion osVersion = new OsVersion(version, cloud);
            Set certifiedVersions = readCertified();
            Optional matching = certifiedVersions.stream()
                                                                     .filter(cv -> cv.osVersion().equals(osVersion))
                                                                     .findFirst();
            if (matching.isPresent()) {
                return matching.get();
            }
            certifiedVersions = new HashSet<>(certifiedVersions);
            certifiedVersions.add(new CertifiedOsVersion(osVersion, vespaVersion));
            curator().writeCertifiedOsVersions(certifiedVersions);
            return new CertifiedOsVersion(osVersion, vespaVersion);
        }
    }

    /** Revoke certification of an OS version */
    public void uncertify(Version version, CloudName cloud) {
        try (Mutex lock = curator().lockCertifiedOsVersions()) {
            OsVersion osVersion = new OsVersion(version, cloud);
            Set certifiedVersions = readCertified();
            Optional existing = certifiedVersions.stream()
                                                                     .filter(cv -> cv.osVersion().equals(osVersion))
                                                                     .findFirst();
            if (existing.isEmpty()) {
                throw new IllegalArgumentException(osVersion + " is not certified");
            }
            certifiedVersions = new HashSet<>(certifiedVersions);
            certifiedVersions.remove(existing.get());
            curator().writeCertifiedOsVersions(certifiedVersions);
        }
    }

    /** Remove certifications for non-existent OS versions */
    public void removeStaleCertifications(OsVersionStatus currentStatus) {
        try (Mutex lock = curator().lockCertifiedOsVersions()) {
            Map oldestVersionByCloud = currentStatus.versions().keySet().stream()
                                                                        .filter(v -> !v.version().isEmpty())
                                                                        .collect(Collectors.toMap(OsVersion::cloud,
                                                                                                  OsVersion::version,
                                                                                                  BinaryOperator.minBy(Comparator.naturalOrder())));
            if (oldestVersionByCloud.isEmpty()) return;

            Set certifiedVersions = new HashSet<>(readCertified());
            boolean modified = certifiedVersions.removeIf(certifiedVersion -> {
                Version oldestVersion = oldestVersionByCloud.get(certifiedVersion.osVersion().cloud());
                return oldestVersion == null || certifiedVersion.osVersion().version().isBefore(oldestVersion);
            });
            if (modified) {
                curator().writeCertifiedOsVersions(certifiedVersions);
            }
        }
    }

    /** Returns whether given OS version is certified as compatible with the current system version */
    public boolean certified(OsVersion osVersion) {
        if (controller.system().isCd()) return true; // Always certified (this is the system doing the certifying)

        Version systemVersion = controller.readSystemVersion();
        return readCertified().stream()
                              .anyMatch(certifiedOsVersion -> certifiedOsVersion.osVersion().equals(osVersion) &&
                                                              // A later system version is fine, as we don't guarantee that
                                                              // an OS upgrade will always coincide with a Vespa release
                                                              !certifiedOsVersion.vespaVersion().isAfter(systemVersion));
    }

    /** Returns all certified versions */
    public Set readCertified() {
        return controller.curator().readCertifiedOsVersions();
    }

    private void requireCloud(CloudName cloud) {
        if (!controller.clouds().contains(cloud)) {
            throw new IllegalArgumentException("Cloud '" + cloud + "' does not exist in this system");
        }
    }

    private void requireNonEmpty(Version version) {
        if (version.isEmpty()) {
            throw new IllegalArgumentException("Invalid version '" + version.toFullString() + "'");
        }
    }

    private CuratorDb curator() {
        return controller.curator();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy