You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by jo...@apache.org on 2018/08/01 23:27:00 UTC

[ambari] branch trunk updated: [AMBARI-24397] - Allow PATCH VDFs to Specify Services Which Are Not Installed in the Cluster (#1943)

This is an automated email from the ASF dual-hosted git repository.

jonathanhurley pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new d8004b0  [AMBARI-24397] - Allow PATCH VDFs to Specify Services Which Are Not Installed in the Cluster (#1943)
d8004b0 is described below

commit d8004b05060a71623b849df2c4c2ad8351a5e6eb
Author: Jonathan Hurley <jo...@apache.org>
AuthorDate: Wed Aug 1 19:26:57 2018 -0400

    [AMBARI-24397] - Allow PATCH VDFs to Specify Services Which Are Not Installed in the Cluster (#1943)
---
 .../ClusterStackVersionResourceProvider.java       |  37 -----
 .../internal/UpgradeResourceProvider.java          |   4 +-
 .../orm/entities/RepositoryVersionEntity.java      |   3 +-
 .../upgrades/FinalizeUpgradeAction.java            |  62 ++++++++-
 .../apache/ambari/server/state/UpgradeContext.java |   7 +-
 .../ClusterStackVersionResourceProviderTest.java   | 103 +-------------
 .../serveraction/upgrades/UpgradeActionTest.java   | 155 +++++++++++++++++++++
 7 files changed, 220 insertions(+), 151 deletions(-)

diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProvider.java
index c9a826f..a78a947 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProvider.java
@@ -20,7 +20,6 @@ package org.apache.ambari.server.controller.internal;
 import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JDK_LOCATION;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
@@ -79,8 +78,6 @@ import org.apache.ambari.server.state.RepositoryVersionState;
 import org.apache.ambari.server.state.ServiceComponentHost;
 import org.apache.ambari.server.state.ServiceOsSpecific;
 import org.apache.ambari.server.state.StackId;
-import org.apache.ambari.server.state.StackInfo;
-import org.apache.ambari.server.state.repository.AvailableService;
 import org.apache.ambari.server.state.repository.ClusterVersionSummary;
 import org.apache.ambari.server.state.repository.VersionDefinitionXml;
 import org.apache.ambari.server.state.stack.upgrade.RepositoryVersionHelper;
@@ -521,8 +518,6 @@ public class ClusterStackVersionResourceProvider extends AbstractControllerResou
       }
     }
 
-    checkPatchVDFAvailableServices(cluster, repoVersionEntity, versionDefinitionXml);
-
     // the cluster will create/update all of the host versions to the correct state
     List<Host> hostsNeedingInstallCommands = cluster.transitionHostsToInstalling(
         repoVersionEntity, versionDefinitionXml, forceInstalled);
@@ -650,38 +645,6 @@ public class ClusterStackVersionResourceProvider extends AbstractControllerResou
     return req;
   }
 
-  /**
-   * Reject PATCH VDFs with Services that are not included in the Cluster
-   * @param cluster cluster instance
-   * @param repoVersionEnt repo version entity
-   * @param desiredVersionDefinition VDF
-   * @throws IllegalArgumentException thrown if VDF includes services that are not installed
-   * @throws AmbariException thrown if could not load stack for repo repoVersionEnt
-   */
-  protected void checkPatchVDFAvailableServices(Cluster cluster, RepositoryVersionEntity repoVersionEnt,
-                                              VersionDefinitionXml desiredVersionDefinition) throws SystemException, AmbariException {
-    if (repoVersionEnt.getType() == RepositoryType.PATCH) {
-
-      Collection<String> notPresentServices = new ArrayList<>();
-      Collection<String> presentServices = new ArrayList<>();
-
-      presentServices.addAll(cluster.getServices().keySet());
-      final StackInfo stack;
-      stack = metaInfo.get().getStack(repoVersionEnt.getStackName(), repoVersionEnt.getStackVersion());
-
-      for (AvailableService availableService : desiredVersionDefinition.getAvailableServices(stack)) {
-        String name = availableService.getName();
-        if (!presentServices.contains(name)) {
-          notPresentServices.add(name);
-        }
-      }
-      if (!notPresentServices.isEmpty()) {
-        throw new IllegalArgumentException(String.format("%s VDF includes services that are not installed: %s",
-            RepositoryType.PATCH, StringUtils.join(notPresentServices, ",")));
-      }
-    }
-  }
-
   @Transactional
   ActionExecutionContext getHostVersionInstallCommand(RepositoryVersionEntity repoVersion,
       Cluster cluster, AmbariManagementController managementController, AmbariMetaInfo ami,
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UpgradeResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UpgradeResourceProvider.java
index 287e1a5..31cd926 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UpgradeResourceProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UpgradeResourceProvider.java
@@ -1603,12 +1603,12 @@ public class UpgradeResourceProvider extends AbstractControllerResourceProvider
 
         // depending on whether this is an upgrade or a downgrade, the history
         // will be different
-        if (upgradeContext.getDirection() == Direction.UPGRADE || upgradeContext.isPatchRevert()) {
+        if (upgradeContext.getDirection() == Direction.UPGRADE) {
           history.setFromRepositoryVersion(component.getDesiredRepositoryVersion());
           history.setTargetRepositoryVersion(upgradeContext.getRepositoryVersion());
         } else {
           // the target version on a downgrade is the original version that the
-          // service was on in the failed upgrade
+          // service was on in the upgrade
           RepositoryVersionEntity targetRepositoryVersion = upgradeContext.getTargetRepositoryVersion(
               serviceName);
 
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/RepositoryVersionEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/RepositoryVersionEntity.java
index 18ac2c2..86359a8 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/RepositoryVersionEntity.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/RepositoryVersionEntity.java
@@ -54,6 +54,7 @@ import org.apache.ambari.server.state.repository.VersionDefinitionXml;
 import org.apache.ambari.server.state.stack.upgrade.RepositoryVersionHelper;
 import org.apache.commons.lang.StringUtils;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -392,7 +393,7 @@ public class RepositoryVersionEntity {
    */
   @Override
   public String toString() {
-    return Objects.toStringHelper(this).add("id", id).add("stack", stack).add("version",
+    return MoreObjects.toStringHelper(this).add("id", id).add("stack", stack).add("version",
         version).add("type", type).add("hidden", isHidden == 1).toString();
   }
 
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/upgrades/FinalizeUpgradeAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/upgrades/FinalizeUpgradeAction.java
index 755f28e..cb6c71e 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/upgrades/FinalizeUpgradeAction.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/upgrades/FinalizeUpgradeAction.java
@@ -35,7 +35,6 @@ 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.metadata.RoleCommandOrderProvider;
 import org.apache.ambari.server.orm.dao.HostComponentStateDAO;
 import org.apache.ambari.server.orm.dao.HostVersionDAO;
 import org.apache.ambari.server.orm.entities.HostComponentStateEntity;
@@ -50,8 +49,11 @@ 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.StackInfo;
 import org.apache.ambari.server.state.UpgradeContext;
 import org.apache.ambari.server.state.UpgradeState;
+import org.apache.ambari.server.state.repository.AvailableService;
+import org.apache.ambari.server.state.repository.VersionDefinitionXml;
 import org.apache.ambari.server.state.stack.upgrade.Direction;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.builder.EqualsBuilder;
@@ -95,9 +97,6 @@ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction {
     }
   }
 
-  @Inject
-  private RoleCommandOrderProvider roleCommandOrderProvider;
-
   /**
    * Execution path for upgrade.
    * @return the command report
@@ -200,7 +199,7 @@ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction {
 
       // move host versions from CURRENT to INSTALLED if their repos are no
       // longer used
-      finalizeHostVersionsNotDesired(cluster);
+      finalizeHostVersionsNotDesired(cluster, upgradeContext);
 
       if (upgradeContext.getOrchestrationType() == RepositoryType.STANDARD) {
         outSB.append(String.format("Finalizing the version for cluster %s.\n", cluster.getClusterName()));
@@ -280,7 +279,7 @@ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction {
         throw new AmbariException(messageBuff.toString());
       }
 
-      finalizeHostVersionsNotDesired(cluster);
+      finalizeHostVersionsNotDesired(cluster, upgradeContext);
 
       // for every repository being downgraded to, ensure the host versions are correct
       Map<String, RepositoryVersionEntity> targetVersionsByService = upgradeContext.getTargetVersions();
@@ -400,11 +399,19 @@ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction {
    * {@link RepositoryVersionState#INSTALLED} or
    * {@link RepositoryVersionState#NOT_REQUIRED} if their assocaited
    * repositories are no longer in use.
+   * <p/>
+   * If this is a patch reversion, then this method will also attempt to
+   * determine if the downgrade-from repository needs to be set to
+   * {@link RepositoryVersionState#OUT_OF_SYNC}. If a patch upgrade was
+   * completed and then a service added after that happens to be specified in
+   * the VDF, then we must mark the repository as OUT_OF_SYNC since those
+   * service's packages were never distributed.
    *
    * @param cluster
    * @throws AmbariException
    */
-  private void finalizeHostVersionsNotDesired(Cluster cluster) throws AmbariException {
+  private void finalizeHostVersionsNotDesired(Cluster cluster, UpgradeContext upgradeContext)
+      throws AmbariException {
     // create a set of all of the repos that the services are on
     Set<RepositoryVersionEntity> desiredRepoVersions = new HashSet<>();
     Set<String> serviceNames = cluster.getServices().keySet();
@@ -425,6 +432,47 @@ public class FinalizeUpgradeAction extends AbstractUpgradeServerAction {
         hostVersion = hostVersionDAO.merge(hostVersion);
       }
     }
+
+    // reverting a patch requires us to check the repository state and
+    // possibly set it to OUT_OF_SYNC if services were added after this patch
+    // was originally distributed
+    if (upgradeContext.isPatchRevert()) {
+      RepositoryVersionEntity repositoryVersionEntity = upgradeContext.getRepositoryVersion();
+
+      final VersionDefinitionXml vdfXml;
+      try {
+        vdfXml = repositoryVersionEntity.getRepositoryXml();
+      } catch (Exception exception) {
+        throw new AmbariException("The VDF's XML could not be deserialized", exception);
+      }
+
+      StackInfo stack = ambariMetaInfo.getStack(repositoryVersionEntity.getStackId());
+
+      // grab the services in the VDF, the services which were part of the
+      // revert, and the services in the cluster to determine if a service was
+      // added to the cluster after the patch was originally applied
+      Collection<AvailableService> availableServices = vdfXml.getAvailableServices(stack);
+      Set<String> participatingServices = upgradeContext.getSupportedServices();
+      Set<String> clusterServices = cluster.getServices().keySet();
+
+      boolean resetRepoStateToOutOfSync = false;
+      for (AvailableService availableService : availableServices) {
+        if (clusterServices.contains(availableService.getName())
+            && !participatingServices.contains(availableService.getName())) {
+          resetRepoStateToOutOfSync = true;
+          break;
+        }
+      }
+
+      if (resetRepoStateToOutOfSync) {
+        List<HostVersionEntity> hostVersions = hostVersionDAO.findHostVersionByClusterAndRepository(
+            cluster.getClusterId(), repositoryVersionEntity);
+        for (HostVersionEntity hostVersion : hostVersions) {
+          hostVersion.setState(RepositoryVersionState.OUT_OF_SYNC);
+          hostVersion = hostVersionDAO.merge(hostVersion);
+        }
+      }
+    }
   }
 
   protected static class InfoTuple implements Comparable<InfoTuple> {
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/UpgradeContext.java b/ambari-server/src/main/java/org/apache/ambari/server/state/UpgradeContext.java
index 8ef648c..0ccc15e 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/UpgradeContext.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/UpgradeContext.java
@@ -387,7 +387,8 @@ public class UpgradeContext {
         throw new AmbariException(message);
       }
 
-      m_repositoryVersion = priors.iterator().next();
+      // the "associated" repository of the revert is the target of what's being reverted
+      m_repositoryVersion = revertUpgrade.getRepositoryVersion();
 
       // !!! the version is used later in validators
       upgradeRequestMap.put(UPGRADE_REPO_VERSION_ID, m_repositoryVersion.getId().toString());
@@ -1061,11 +1062,11 @@ public class UpgradeContext {
 
   /**
    * Gets the set of services which will participate in the upgrade. The
-   * services available in the repository are comapred against those installed
+   * services available in the repository are compared against those installed
    * in the cluster to arrive at the final subset.
    * <p/>
    * In some cases, such as with a {@link RepositoryType#MAINT} repository, the
-   * subset can be further trimmed by determing that an installed services is
+   * subset can be further trimmed by determing that an installed service is
    * already at a high enough version and doesn't need to be upgraded.
    * <p/>
    * This method will also populate the source ({@link #m_sourceRepositoryMap})
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProviderTest.java
index ecf629f..f26db2a 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProviderTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ClusterStackVersionResourceProviderTest.java
@@ -34,7 +34,6 @@ import java.io.FileInputStream;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -78,8 +77,8 @@ import org.apache.ambari.server.orm.dao.HostVersionDAO;
 import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
 import org.apache.ambari.server.orm.entities.ClusterEntity;
 import org.apache.ambari.server.orm.entities.HostVersionEntity;
- import org.apache.ambari.server.orm.entities.RepoDefinitionEntity;
- import org.apache.ambari.server.orm.entities.RepoOsEntity;
+import org.apache.ambari.server.orm.entities.RepoDefinitionEntity;
+import org.apache.ambari.server.orm.entities.RepoOsEntity;
 import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
 import org.apache.ambari.server.orm.entities.StackEntity;
 import org.apache.ambari.server.orm.entities.UpgradeEntity;
@@ -98,9 +97,7 @@ import org.apache.ambari.server.state.ServiceComponentHost;
 import org.apache.ambari.server.state.ServiceInfo;
 import org.apache.ambari.server.state.ServiceOsSpecific;
 import org.apache.ambari.server.state.StackId;
-import org.apache.ambari.server.state.StackInfo;
 import org.apache.ambari.server.state.cluster.ClusterImpl;
-import org.apache.ambari.server.state.repository.AvailableService;
 import org.apache.ambari.server.state.repository.ClusterVersionSummary;
 import org.apache.ambari.server.state.repository.VersionDefinitionXml;
 import org.apache.ambari.server.state.stack.upgrade.Direction;
@@ -127,8 +124,6 @@ import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.util.Modules;
 
-import junit.framework.AssertionFailedError;
-
 
  /**
  * ClusterStackVersionResourceProvider tests.
@@ -1747,100 +1742,6 @@ public class ClusterStackVersionResourceProviderTest {
 
   }
 
-  @Test
-  public void testCheckPatchVDFAvailableServices() throws Exception {
-    Cluster cluster = createNiceMock(Cluster.class);
-    RepositoryVersionEntity repoVersionEnt = createNiceMock(RepositoryVersionEntity.class);
-    VersionDefinitionXml desiredVersionDefinition = createNiceMock(VersionDefinitionXml.class);
-
-    expect(repoVersionEnt.getType()).andReturn(RepositoryType.PATCH).once();
-
-    Service service1 = createNiceMock(Service.class);
-    expect(service1.getName()).andReturn("SERVICE1").anyTimes();
-    expect(service1.getServiceComponents()).andReturn(new HashMap<String, ServiceComponent>());
-
-    Service service2 = createNiceMock(Service.class);
-    expect(service2.getName()).andReturn("SERVICE2").anyTimes();
-    expect(service2.getServiceComponents()).andReturn(new HashMap<String, ServiceComponent>());
-
-    Map<String, Service> clusterServices = new HashMap<>();
-    clusterServices.put("SERVICE1",service1);
-    clusterServices.put("SERVICE2",service2);
-
-    expect(cluster.getServices()).andReturn(clusterServices).once();
-    expect(repoVersionEnt.getStackName()).andReturn("HDP").once();
-    expect(repoVersionEnt.getStackVersion()).andReturn("2.5.0").once();
-
-    AvailableService availableService1 = createNiceMock(AvailableService.class);
-    expect(availableService1.getName()).andReturn("SERVICE1").anyTimes();
-
-    AvailableService availableService2 = createNiceMock(AvailableService.class);
-    expect(availableService2.getName()).andReturn("SERVICE2").anyTimes();
-
-    Collection<AvailableService> availableServices = new ArrayList<>();
-    availableServices.add(availableService1);
-    availableServices.add(availableService2);
-
-    expect(desiredVersionDefinition.getAvailableServices((StackInfo)EasyMock.anyObject())).andReturn(availableServices).once();
-
-    expect(cluster.transitionHostsToInstalling(
-        anyObject(RepositoryVersionEntity.class), anyObject(VersionDefinitionXml.class),
-        EasyMock.anyBoolean())).andReturn(Collections.<Host>emptyList()).atLeastOnce();
-
-    replay(cluster, repoVersionEnt, desiredVersionDefinition, service1, service2, availableService1, availableService2);
-
-    ClusterStackVersionResourceProvider provider = new ClusterStackVersionResourceProvider(null);
-    injector.injectMembers(provider);
-    provider.checkPatchVDFAvailableServices(cluster, repoVersionEnt, desiredVersionDefinition);
-  }
-
-   @Test
-   public void testCheckPatchVDFAvailableServicesFail() throws Exception {
-     Cluster cluster = createNiceMock(Cluster.class);
-     RepositoryVersionEntity repoVersionEnt = createNiceMock(RepositoryVersionEntity.class);
-     VersionDefinitionXml desiredVersionDefinition = createNiceMock(VersionDefinitionXml.class);
-
-     expect(repoVersionEnt.getType()).andReturn(RepositoryType.PATCH).once();
-
-     Service service1 = createNiceMock(Service.class);
-     expect(service1.getName()).andReturn("SERVICE1").anyTimes();
-     expect(service1.getServiceComponents()).andReturn(new HashMap<String, ServiceComponent>());
-
-     Map<String, Service> clusterServices = new HashMap<>();
-     clusterServices.put("SERVICE1",service1);
-
-     expect(cluster.getServices()).andReturn(clusterServices).once();
-     expect(repoVersionEnt.getStackName()).andReturn("HDP").once();
-     expect(repoVersionEnt.getStackVersion()).andReturn("2.5.0").once();
-
-     AvailableService availableService1 = createNiceMock(AvailableService.class);
-     expect(availableService1.getName()).andReturn("SERVICE1").anyTimes();
-
-     AvailableService availableService2 = createNiceMock(AvailableService.class);
-     expect(availableService2.getName()).andReturn("SERVICE2").anyTimes();
-
-     Collection<AvailableService> availableServices = new ArrayList<>();
-     availableServices.add(availableService1);
-     availableServices.add(availableService2);
-
-     expect(desiredVersionDefinition.getAvailableServices((StackInfo)EasyMock.anyObject())).andReturn(availableServices).once();
-     expect(cluster.transitionHostsToInstalling(
-         anyObject(RepositoryVersionEntity.class), anyObject(VersionDefinitionXml.class),
-         EasyMock.anyBoolean())).andThrow(new AssertionFailedError()).anyTimes();
-
-     replay(cluster, repoVersionEnt, desiredVersionDefinition, service1, availableService1, availableService2);
-
-     ClusterStackVersionResourceProvider provider = new ClusterStackVersionResourceProvider(null);
-     injector.injectMembers(provider);
-     try {
-       provider.checkPatchVDFAvailableServices(cluster, repoVersionEnt, desiredVersionDefinition);
-       Assert.fail("Expected an exception when PATCH VDF includes services that are not installed");
-     } catch (IllegalArgumentException expected) {
-       // !!! expected
-       Assert.assertEquals(expected.getMessage(),"PATCH VDF includes services that are not installed: SERVICE2");
-     }
-   }
-
    private void testCreateResourcesExistingUpgrade(Authentication authentication) throws Exception {
     Cluster cluster = createNiceMock(Cluster.class);
 
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/upgrades/UpgradeActionTest.java b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/upgrades/UpgradeActionTest.java
index 86507b9..8ba5250 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/upgrades/UpgradeActionTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/upgrades/UpgradeActionTest.java
@@ -21,7 +21,10 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import java.io.File;
+import java.io.FileInputStream;
 import java.lang.reflect.Field;
+import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -66,6 +69,7 @@ import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.Config;
 import org.apache.ambari.server.state.ConfigFactory;
 import org.apache.ambari.server.state.Host;
+import org.apache.ambari.server.state.RepositoryType;
 import org.apache.ambari.server.state.RepositoryVersionState;
 import org.apache.ambari.server.state.Service;
 import org.apache.ambari.server.state.ServiceComponent;
@@ -77,8 +81,10 @@ import org.apache.ambari.server.state.StackId;
 import org.apache.ambari.server.state.State;
 import org.apache.ambari.server.state.UpgradeState;
 import org.apache.ambari.server.state.stack.UpgradePack;
+import org.apache.ambari.server.state.stack.upgrade.Direction;
 import org.apache.ambari.server.state.stack.upgrade.UpgradeType;
 import org.apache.ambari.server.utils.EventBusSynchronizer;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.junit.After;
 import org.junit.Assert;
@@ -141,6 +147,9 @@ public class UpgradeActionTest {
   private ConfigFactory configFactory;
 
   @Inject
+  private RepositoryVersionDAO repositoryVersionDAO;
+
+  @Inject
   private HostComponentStateDAO hostComponentStateDAO;
 
   private RepositoryVersionEntity repositoryVersion2110;
@@ -583,6 +592,110 @@ public class UpgradeActionTest {
     }
   }
 
+  /**
+   * Tests the case where a revert happens on a patch upgrade and a new service
+   * has been added which causes the repository to go OUT_OF_SYNC.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testHostVersionsOutOfSyncAfterRevert() throws Exception {
+    String hostName = "h1";
+    Cluster cluster = createUpgradeCluster(repositoryVersion2110, hostName);
+    createHostVersions(repositoryVersion2111, hostName);
+
+    // Install ZK with some components (HBase is installed later to test the
+    // logic of revert)
+    Service zk = installService(cluster, "ZOOKEEPER", repositoryVersion2110);
+    addServiceComponent(cluster, zk, "ZOOKEEPER_SERVER");
+    addServiceComponent(cluster, zk, "ZOOKEEPER_CLIENT");
+    createNewServiceComponentHost(cluster, "ZOOKEEPER", "ZOOKEEPER_SERVER", hostName);
+    createNewServiceComponentHost(cluster, "ZOOKEEPER", "ZOOKEEPER_CLIENT", hostName);
+
+    List<HostVersionEntity> hostVersions = hostVersionDAO.findAll();
+    assertEquals(2, hostVersions.size());
+
+    // repo 2110 - CURRENT
+    // repo 2111 - CURRENT (PATCH)
+    for (HostVersionEntity hostVersion : hostVersions) {
+      RepositoryVersionEntity hostRepoVersion = hostVersion.getRepositoryVersion();
+      if (repositoryVersion2110.equals(hostRepoVersion)) {
+        hostVersion.setState(RepositoryVersionState.CURRENT);
+      } else {
+        hostVersion.setState(RepositoryVersionState.CURRENT);
+      }
+
+      hostVersionDAO.merge(hostVersion);
+    }
+
+    // convert the repository into a PATCH repo (which should have ZK and HBase
+    // as available services)
+    repositoryVersion2111.setParent(repositoryVersion2110);
+    repositoryVersion2111.setType(RepositoryType.PATCH);
+    repositoryVersion2111.setVersionXml(hostName);
+    repositoryVersion2111.setVersionXsd("version_definition.xsd");
+
+    File patchVdfFile = new File("src/test/resources/hbase_version_test.xml");
+    repositoryVersion2111.setVersionXml(
+        IOUtils.toString(new FileInputStream(patchVdfFile), Charset.defaultCharset()));
+
+    repositoryVersion2111 = repositoryVersionDAO.merge(repositoryVersion2111);
+
+    // pretend like we patched
+    UpgradeEntity upgrade = createUpgrade(cluster, repositoryVersion2111);
+    upgrade.setOrchestration(RepositoryType.PATCH);
+    upgrade.setRevertAllowed(true);
+    upgrade = upgradeDAO.merge(upgrade);
+
+    // add a service on the parent repo to cause the OUT_OF_SYNC on revert
+    Service hbase = installService(cluster, "HBASE", repositoryVersion2110);
+    addServiceComponent(cluster, hbase, "HBASE_MASTER");
+    createNewServiceComponentHost(cluster, "HBASE", "HBASE_MASTER", hostName);
+
+    // revert the patch
+    UpgradeEntity revert = createRevert(cluster, upgrade);
+    assertEquals(RepositoryType.PATCH, revert.getOrchestration());
+
+    // push all services to the revert repo version for finalize
+    Map<String, Service> services = cluster.getServices();
+    assertTrue(services.size() > 0);
+    for (Service service : services.values()) {
+      service.setDesiredRepositoryVersion(repositoryVersion2110);
+    }
+
+    // push all components to the revert version
+    List<HostComponentStateEntity> hostComponentStates = hostComponentStateDAO.findByHost(hostName);
+    for (HostComponentStateEntity hostComponentState : hostComponentStates) {
+      hostComponentState.setVersion(repositoryVersion2110.getVersion());
+      hostComponentStateDAO.merge(hostComponentState);
+    }
+
+    Map<String, String> commandParams = new HashMap<>();
+    ExecutionCommand executionCommand = new ExecutionCommand();
+    executionCommand.setCommandParams(commandParams);
+    executionCommand.setClusterName(clusterName);
+
+    HostRoleCommand hostRoleCommand = hostRoleCommandFactory.create(null, null, null, null);
+    hostRoleCommand.setExecutionCommandWrapper(new ExecutionCommandWrapper(executionCommand));
+
+    finalizeUpgradeAction.setExecutionCommand(executionCommand);
+    finalizeUpgradeAction.setHostRoleCommand(hostRoleCommand);
+
+    // finalize
+    CommandReport report = finalizeUpgradeAction.execute(null);
+    assertNotNull(report);
+    assertEquals(HostRoleStatus.COMPLETED.name(), report.getStatus());
+
+    for (HostVersionEntity hostVersion : hostVersions) {
+      RepositoryVersionEntity hostRepoVersion = hostVersion.getRepositoryVersion();
+      if (repositoryVersion2110.equals(hostRepoVersion)) {
+        assertEquals(RepositoryVersionState.CURRENT, hostVersion.getState());
+      } else {
+        assertEquals(RepositoryVersionState.OUT_OF_SYNC, hostVersion.getState());
+      }
+    }
+  }
+
   private ServiceComponentHost createNewServiceComponentHost(Cluster cluster, String svc,
                                                              String svcComponent, String hostName) throws AmbariException {
     Assert.assertNotNull(cluster.getConfigGroups());
@@ -692,4 +805,46 @@ public class UpgradeActionTest {
     cluster.setUpgradeEntity(upgradeEntity);
     return upgradeEntity;
   }
+
+  /**
+   * Creates a revert based on an existing upgrade.
+   */
+  private UpgradeEntity createRevert(Cluster cluster, UpgradeEntity upgradeToRevert)
+      throws Exception {
+
+    // create some entities for the finalize action to work with for patch
+    // history
+    RequestEntity requestEntity = new RequestEntity();
+    requestEntity.setClusterId(cluster.getClusterId());
+    requestEntity.setRequestId(2L);
+    requestEntity.setStartTime(System.currentTimeMillis());
+    requestEntity.setCreateTime(System.currentTimeMillis());
+    requestDAO.create(requestEntity);
+
+    UpgradeEntity revert = new UpgradeEntity();
+    revert.setId(2L);
+    revert.setDirection(Direction.DOWNGRADE);
+    revert.setClusterId(cluster.getClusterId());
+    revert.setRequestEntity(requestEntity);
+    revert.setUpgradePackage("");
+    revert.setRepositoryVersion(upgradeToRevert.getRepositoryVersion());
+    revert.setUpgradeType(upgradeToRevert.getUpgradeType());
+    revert.setOrchestration(upgradeToRevert.getOrchestration());
+
+
+    for (UpgradeHistoryEntity historyToRevert : upgradeToRevert.getHistory()) {
+      UpgradeHistoryEntity history = new UpgradeHistoryEntity();
+      history.setUpgrade(revert);
+      history.setServiceName(historyToRevert.getServiceName());
+      history.setComponentName(historyToRevert.getComponentName());
+      history.setFromRepositoryVersion(upgradeToRevert.getRepositoryVersion());
+      history.setTargetRepositoryVersion(historyToRevert.getFromReposistoryVersion());
+      revert.addHistory(history);
+
+    }
+    upgradeDAO.create(revert);
+    cluster.setUpgradeEntity(revert);
+    return revert;
+  }
+
 }