/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ambari.server.serveraction.upgrades; import java.io.PrintWriter; import java.io.StringWriter; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.actionmanager.HostRoleStatus; import org.apache.ambari.server.agent.CommandReport; import org.apache.ambari.server.api.services.AmbariMetaInfo; import org.apache.ambari.server.events.StackUpgradeFinishEvent; import org.apache.ambari.server.events.publishers.VersionEventPublisher; import org.apache.ambari.server.orm.dao.ClusterVersionDAO; import org.apache.ambari.server.orm.dao.HostComponentStateDAO; import org.apache.ambari.server.orm.dao.HostVersionDAO; import org.apache.ambari.server.orm.dao.ServiceComponentDesiredStateDAO; import org.apache.ambari.server.orm.dao.StackDAO; import org.apache.ambari.server.orm.dao.UpgradeDAO; import org.apache.ambari.server.orm.entities.ClusterVersionEntity; import org.apache.ambari.server.orm.entities.HostComponentStateEntity; import org.apache.ambari.server.orm.entities.HostEntity; import org.apache.ambari.server.orm.entities.HostVersionEntity; import org.apache.ambari.server.orm.entities.RepositoryVersionEntity; import org.apache.ambari.server.orm.entities.ServiceComponentDesiredStateEntity; import org.apache.ambari.server.orm.entities.ServiceComponentHistoryEntity; import org.apache.ambari.server.orm.entities.StackEntity; import org.apache.ambari.server.orm.entities.UpgradeEntity; import org.apache.ambari.server.state.Cluster; import org.apache.ambari.server.state.ComponentInfo; import org.apache.ambari.server.state.RepositoryVersionState; import org.apache.ambari.server.state.Service; import org.apache.ambari.server.state.ServiceComponent; import org.apache.ambari.server.state.ServiceComponentHost; import org.apache.ambari.server.state.StackId; import org.apache.ambari.server.state.UpgradeState; import org.apache.ambari.server.state.svccomphost.ServiceComponentHostSummary; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.text.StrBuilder; import com.google.inject.Inject; /** * Action that represents finalizing the Upgrade by completing any database changes. */ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction { public static final String PREVIOUS_UPGRADE_NOT_COMPLETED_MSG = "It is possible that a previous upgrade was not finalized. " + "For this reason, Ambari will not remove any configs. Please ensure that all database records are correct."; @Inject private ClusterVersionDAO clusterVersionDAO; @Inject private HostVersionDAO hostVersionDAO; @Inject private HostComponentStateDAO hostComponentStateDAO; /** * Gets {@link StackEntity} instances from {@link StackId}. */ @Inject private StackDAO stackDAO; /** * Gets desired state entities for service components. */ @Inject private ServiceComponentDesiredStateDAO serviceComponentDesiredStateDAO; /** * Gets {@link UpgradeEntity} instances. */ @Inject private UpgradeDAO upgradeDAO; @Inject private AmbariMetaInfo ambariMetaInfo; @Inject VersionEventPublisher versionEventPublisher; @Override public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException { Map<String, String> commandParams = getExecutionCommand().getCommandParams(); boolean isDowngrade = commandParams.containsKey(UPGRADE_DIRECTION_KEY) && "downgrade".equals(commandParams.get(UPGRADE_DIRECTION_KEY).toLowerCase()); String version = commandParams.get(VERSION_KEY); StackId originalStackId = new StackId(commandParams.get(ORIGINAL_STACK_KEY)); StackId targetStackId = new StackId(commandParams.get(TARGET_STACK_KEY)); String clusterName = getExecutionCommand().getClusterName(); if (isDowngrade) { return finalizeDowngrade(clusterName, originalStackId, targetStackId, version); } else { return finalizeUpgrade(clusterName, version, commandParams); } } /** * Execution path for upgrade. * @param clusterName the name of the cluster the upgrade is for * @param version the target version of the upgrade * @return the command report */ private CommandReport finalizeUpgrade(String clusterName, String version, Map<String, String> commandParams) throws AmbariException, InterruptedException { StringBuilder outSB = new StringBuilder(); StringBuilder errSB = new StringBuilder(); try { outSB.append(MessageFormat.format("Begin finalizing the upgrade of cluster {0} to version {1}\n", clusterName, version)); Cluster cluster = m_clusters.getCluster(clusterName); StackId clusterDesiredStackId = cluster.getDesiredStackVersion(); StackId clusterCurrentStackId = cluster.getCurrentStackVersion(); ClusterVersionEntity upgradingClusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion( clusterName, clusterDesiredStackId, version); if (upgradingClusterVersion == null) { throw new AmbariException(MessageFormat.format( "Cluster stack version {0} not found", version)); } // Validate that all of the hosts with a version in the cluster have the // version being upgraded to, and it is in an allowed state. List<HostVersionEntity> hostVersions = hostVersionDAO.findByClusterStackAndVersion( clusterName, clusterDesiredStackId, version); // Will include hosts whose state is INSTALLED Set<HostVersionEntity> hostVersionsAllowed = new HashSet<>(); Set<String> hostsWithoutCorrectVersionState = new HashSet<>(); Set<String> hostsToUpdate = new HashSet<>(); // It is important to only iterate over the hosts with a version, as // opposed to all hosts, since some hosts may only have components that do // not advertise a version, such as AMBARI_METRICS. for (HostVersionEntity hostVersion : hostVersions) { boolean hostHasCorrectVersionState = false; RepositoryVersionState hostVersionState = hostVersion.getState(); switch( hostVersionState ){ case CURRENT:{ // if the state is correct, then do nothing hostHasCorrectVersionState = true; break; } case NOT_REQUIRED: case INSTALLED:{ // It is possible that the host version has a state of INSTALLED and it // never changed if the host only has components that do not advertise a // version. HostEntity host = hostVersion.getHostEntity(); ServiceComponentHostSummary hostSummary = new ServiceComponentHostSummary(ambariMetaInfo, host, clusterDesiredStackId); // if all components have finished advertising their version, then // this host can be considered upgraded if (hostSummary.haveAllComponentsFinishedAdvertisingVersion()) { // mark this as upgraded hostHasCorrectVersionState = true; } else { hostsWithoutCorrectVersionState.add(hostVersion.getHostName()); } break; } default: { // all other states are not allowed hostsWithoutCorrectVersionState.add(hostVersion.getHostName()); break; } } // keep track of this host version in order to transition it correctly if (hostHasCorrectVersionState) { hostVersionsAllowed.add(hostVersion); hostsToUpdate.add(hostVersion.getHostName()); } } // throw an exception if there are hosts which are not not fully upgraded if (hostsWithoutCorrectVersionState.size() > 0) { String message = String.format("The following %d host(s) have not been upgraded to version %s. " + "Please install and upgrade the Stack Version on those hosts and try again.\nHosts: %s\n", hostsWithoutCorrectVersionState.size(), version, StringUtils.join(hostsWithoutCorrectVersionState, ", ")); outSB.append(message); throw new AmbariException(message); } // iterate through all host components and make sure that they are on the // correct version; if they are not, then this will throw an exception List<InfoTuple> errors = checkHostComponentVersions(cluster, version, clusterDesiredStackId); if (! errors.isEmpty()) { StrBuilder messageBuff = new StrBuilder( String.format( "The following %d host component(s) " + "have not been upgraded to version %s. Please install and upgrade " + "the Stack Version on those hosts and try again.\nHost components:\n", errors.size(), version)); for (InfoTuple error : errors) { messageBuff.append(String.format("%s on host %s\n", error.componentName, error.hostName)); } throw new AmbariException(messageBuff.toString()); } // we're guaranteed to be ready transition to upgraded now; ensure that // the transition will be allowed if the cluster state is not upgraded upgradingClusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion(clusterName, clusterDesiredStackId, version); if (RepositoryVersionState.INSTALLING == upgradingClusterVersion.getState()) { cluster.transitionClusterVersion(clusterDesiredStackId, version, RepositoryVersionState.INSTALLED); upgradingClusterVersion = clusterVersionDAO.findByClusterAndStackAndVersion( clusterName, clusterDesiredStackId, version); } // we cannot finalize since the cluster was not ready to move into the // upgraded state if (RepositoryVersionState.INSTALLED != upgradingClusterVersion.getState()) { throw new AmbariException(String.format("The cluster stack version state %s is not allowed to transition directly into %s", upgradingClusterVersion.getState(), RepositoryVersionState.CURRENT.toString())); } outSB.append( String.format("Finalizing the upgraded state of host components in %d host(s).\n", hostVersionsAllowed.size())); // Reset the upgrade state for (HostVersionEntity hostVersion : hostVersionsAllowed) { Collection<HostComponentStateEntity> hostComponentStates = hostComponentStateDAO.findByHost(hostVersion.getHostName()); for (HostComponentStateEntity hostComponentStateEntity: hostComponentStates) { hostComponentStateEntity.setUpgradeState(UpgradeState.NONE); hostComponentStateDAO.merge(hostComponentStateEntity); } } // Impacts all hosts that have a version outSB.append( String.format("Finalizing the version for %d host(s).\n", hostVersionsAllowed.size())); cluster.mapHostVersions(hostsToUpdate, upgradingClusterVersion, RepositoryVersionState.CURRENT); versionEventPublisher.publish(new StackUpgradeFinishEvent(cluster)); // Reset upgrade state cluster.setUpgradeEntity(null); // transitioning the cluster into CURRENT will update the current/desired // stack values outSB.append(String.format("Finalizing the version for cluster %s.\n", clusterName)); cluster.transitionClusterVersion(clusterDesiredStackId, version, RepositoryVersionState.CURRENT); if (commandParams.containsKey(REQUEST_ID)) { String requestId = commandParams.get(REQUEST_ID); UpgradeEntity upgradeEntity = upgradeDAO.findUpgradeByRequestId(Long.valueOf(requestId)); if (null != upgradeEntity) { outSB.append("Creating upgrade history.\n"); writeComponentHistory(cluster, upgradeEntity, clusterCurrentStackId, clusterDesiredStackId); } else { String warning = String.format( "Unable to create upgrade history because no upgrade could be found for request with ID %s\n", requestId); outSB.append(warning); } } outSB.append("Upgrade was successful!\n"); return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", outSB.toString(), errSB.toString()); } catch (Exception e) { errSB.append(e.getMessage()); return createCommandReport(-1, HostRoleStatus.FAILED, "{}", outSB.toString(), errSB.toString()); } } /** * Execution path for downgrade. * * @param clusterName * the name of the cluster the downgrade is for * @paran originalStackId the stack ID of the cluster before the upgrade. * @paran targetStackId the stack ID that was desired for this upgrade. * @param version * the target version of the downgrade * @return the command report */ private CommandReport finalizeDowngrade(String clusterName, StackId originalStackId, StackId targetStackId, String version) throws AmbariException, InterruptedException { StringBuilder out = new StringBuilder(); StringBuilder err = new StringBuilder(); try { Cluster cluster = m_clusters.getCluster(clusterName); StackId currentClusterStackId = cluster.getCurrentStackVersion(); // Safety check that the cluster's stack (from clusterstate's current_stack_id) is equivalent to the // cluster's CURRENT repo version's stack. This is to avoid deleting configs from the target stack if the customer // ended up modifying their database manually after a stack upgrade and forgot to call "Save DB State". ClusterVersionEntity currentClusterVersion = cluster.getCurrentClusterVersion(); RepositoryVersionEntity currentRepoVersion = currentClusterVersion.getRepositoryVersion(); StackId currentRepoStackId = currentRepoVersion.getStackId(); if (!currentRepoStackId.equals(originalStackId)) { String msg = String.format("The stack of Cluster %s's CURRENT repo version is %s, yet the original stack id from " + "the Stack Upgrade has a different value of %s. %s", clusterName, currentRepoStackId.getStackId(), originalStackId.getStackId(), PREVIOUS_UPGRADE_NOT_COMPLETED_MSG); out.append(msg); err.append(msg); throw new AmbariException("The source target stack doesn't match the cluster's CURRENT repo version's stack."); } // This was a cross-stack upgrade, meaning that configurations were created that now need to be removed. if (!originalStackId.equals(targetStackId)) { out.append(String.format("Will remove configs since the original stack %s differs from the target stack %s " + "that Ambari just downgraded from.", originalStackId.getStackId(), targetStackId.getStackId())); cluster.removeConfigurations(targetStackId); } // !!! find and make sure the cluster_version EXCEPT current are set back out.append(String.format("Searching for current version for %s\n", clusterName)); ClusterVersionEntity clusterVersion = clusterVersionDAO.findByClusterAndStateCurrent(clusterName); if (null == clusterVersion) { throw new AmbariException("Could not find current cluster version"); } out.append(String.format("Comparing downgrade version %s to current cluster version %s\n", version, clusterVersion.getRepositoryVersion().getVersion())); if (!version.equals(clusterVersion.getRepositoryVersion().getVersion())) { throw new AmbariException( String.format("Downgrade version %s is not the current cluster version of %s", version, clusterVersion.getRepositoryVersion().getVersion())); } else { out.append(String.format("Downgrade version is the same as current. Searching " + "for cluster versions that do not match %s\n", version)); } Set<String> badVersions = new HashSet<>(); // update the cluster version for (ClusterVersionEntity cve : clusterVersionDAO.findByCluster(clusterName)) { switch (cve.getState()) { case INSTALL_FAILED: case INSTALLED: case INSTALLING: { badVersions.add(cve.getRepositoryVersion().getVersion()); cve.setState(RepositoryVersionState.INSTALLED); clusterVersionDAO.merge(cve); break; } default: break; } } out.append(String.format("Found %d other version(s) not matching downgrade: %s\n", badVersions.size(), StringUtils.join(badVersions, ", "))); Set<String> badHosts = new HashSet<>(); for (String badVersion : badVersions) { List<HostVersionEntity> hostVersions = hostVersionDAO.findByClusterStackAndVersion( clusterName, targetStackId, badVersion); for (HostVersionEntity hostVersion : hostVersions) { badHosts.add(hostVersion.getHostName()); hostVersion.setState(RepositoryVersionState.INSTALLED); hostVersionDAO.merge(hostVersion); } } out.append(String.format("Found %d hosts not matching downgrade version: %s\n", badHosts.size(), version)); for (String badHost : badHosts) { List<HostComponentStateEntity> hostComponentStates = hostComponentStateDAO.findByHost(badHost); for (HostComponentStateEntity hostComponentState : hostComponentStates) { hostComponentState.setUpgradeState(UpgradeState.NONE); hostComponentStateDAO.merge(hostComponentState); } } // ensure that when downgrading, we set the desired back to the // original value cluster.setDesiredStackVersion(currentClusterStackId); versionEventPublisher.publish(new StackUpgradeFinishEvent(cluster)); // Reset upgrade state cluster.setUpgradeEntity(null); return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", out.toString(), err.toString()); } catch (Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); err.append(sw.toString()); return createCommandReport(-1, HostRoleStatus.FAILED, "{}", out.toString(), err.toString()); } } /** * Confirms that all host components that are able to provide hdp version, * have been upgraded to the target version. * @param cluster the cluster the upgrade is for * @param desiredVersion the target version of the upgrade * @param targetStackId the target stack id for meta-info lookup * @return the list of {@link InfoTuple} objects of host components in error */ protected List<InfoTuple> checkHostComponentVersions(Cluster cluster, String desiredVersion, StackId targetStackId) throws AmbariException { ArrayList<InfoTuple> errors = new ArrayList<>(); Set<String> supportedServices = getSupportedServices(); for (Service service : cluster.getServices().values()) { // !!! if there are supported services for upgrade, and the cluster service is NOT in the list, skip if (!supportedServices.isEmpty() && !supportedServices.contains(service.getName())) { continue; } for (ServiceComponent serviceComponent : service.getServiceComponents().values()) { for (ServiceComponentHost serviceComponentHost : serviceComponent.getServiceComponentHosts().values()) { ComponentInfo componentInfo = ambariMetaInfo.getComponent(targetStackId.getStackName(), targetStackId.getStackVersion(), service.getName(), serviceComponent.getName()); if (!componentInfo.isVersionAdvertised()) { StackId desired = serviceComponentHost.getDesiredStackVersion(); StackId actual = serviceComponentHost.getStackVersion(); if (!desired.equals(actual)) { serviceComponentHost.setStackVersion(desired); } } else if (componentInfo.isVersionAdvertised() && !serviceComponentHost.getVersion().equals(desiredVersion)) { errors.add(new InfoTuple( service.getName(), serviceComponent.getName(), serviceComponentHost.getHostName(), serviceComponentHost.getVersion())); } } } } return errors; } private void writeComponentHistory(Cluster cluster, UpgradeEntity upgradeEntity, StackId fromStackId, StackId toStackId) { StackEntity fromStack = stackDAO.find(fromStackId.getStackName(), fromStackId.getStackVersion()); StackEntity toStack = stackDAO.find(toStackId.getStackName(), toStackId.getStackVersion()); // for every service component, if it was included in the upgrade then // create a historical entry for (Service service : cluster.getServices().values()) { for (ServiceComponent serviceComponent : service.getServiceComponents().values()) { if (serviceComponent.isVersionAdvertised()) { // create the historical entry ServiceComponentHistoryEntity historyEntity = new ServiceComponentHistoryEntity(); historyEntity.setUpgrade(upgradeEntity); historyEntity.setFromStack(fromStack); historyEntity.setToStack(toStack); // get the service component ServiceComponentDesiredStateEntity desiredStateEntity = serviceComponentDesiredStateDAO.findByName( cluster.getClusterId(), serviceComponent.getServiceName(), serviceComponent.getName()); // add the history to the component and save desiredStateEntity.addHistory(historyEntity); serviceComponentDesiredStateDAO.merge(desiredStateEntity); } } } } protected static class InfoTuple { protected final String serviceName; protected final String componentName; protected final String hostName; protected final String currentVersion; protected InfoTuple(String service, String component, String host, String version) { serviceName = service; componentName = component; hostName = host; currentVersion = version; } } }