You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by dm...@apache.org on 2015/09/17 13:42:50 UTC

ambari git commit: AMBARI-13100. Stop-and-Start Upgrade: Apply configs from multiple major stack versions. (dlysnichenko)

Repository: ambari
Updated Branches:
  refs/heads/branch-dev-stop-all-upgrade df0d238a1 -> b9b708e6c


AMBARI-13100. Stop-and-Start Upgrade: Apply configs from multiple major stack versions. (dlysnichenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b9b708e6
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b9b708e6
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b9b708e6

Branch: refs/heads/branch-dev-stop-all-upgrade
Commit: b9b708e6c2f603c306eff14b3c303b6daa18655c
Parents: df0d238
Author: Lisnichenko Dmitro <dl...@hortonworks.com>
Authored: Thu Sep 17 14:37:52 2015 +0300
Committer: Lisnichenko Dmitro <dl...@hortonworks.com>
Committed: Thu Sep 17 14:37:52 2015 +0300

----------------------------------------------------------------------
 .../internal/UpgradeResourceProvider.java       |  24 ++-
 .../server/state/stack/ConfigUpgradePack.java   |  67 ++++++--
 .../ambari/server/state/stack/UpgradePack.java  |  21 +++
 .../HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml |   5 +
 .../checks/UpgradeCheckStackVersionTest.java    | 170 -------------------
 .../state/stack/ConfigUpgradePackTest.java      | 148 ++++++++++++++++
 6 files changed, 249 insertions(+), 186 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/UpgradeResourceProvider.java
----------------------------------------------------------------------
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 8f4870c..a2f642c 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
@@ -582,10 +582,24 @@ public class UpgradeResourceProvider extends AbstractControllerResourceProvider
       applyStackAndProcessConfigurations(targetStackId.getStackName(), cluster, version, direction, pack);
     }
 
-    // TODO: for cross-stack upgrade, merge a new config upgrade pack from all
-    // target stacks involved into upgrade and pass it into method
-    ConfigUpgradePack configUpgradePack = s_metaProvider.get().getConfigUpgradePack(
-        targetStackId.getStackName(), targetStackId.getStackVersion());
+    // Resolve or build a proper config upgrade pack
+    List<UpgradePack.IntermediateStack> intermediateStacks = pack.getIntermediateStacks();
+    ConfigUpgradePack configUpgradePack;
+    if (intermediateStacks == null || intermediateStacks.isEmpty()) { // No intermediate stacks
+      configUpgradePack = s_metaProvider.get().getConfigUpgradePack(
+              targetStackId.getStackName(), targetStackId.getStackVersion());
+    } else {
+      // For cross-stack upgrade, follow all major stacks and merge a new config upgrade pack from all
+      // target stacks involved into upgrade
+      ArrayList<ConfigUpgradePack> intermediateConfigUpgradePacks = new ArrayList<>();
+      for (UpgradePack.IntermediateStack intermediateStack : intermediateStacks) {
+        ConfigUpgradePack intermediateConfigUpgradePack = s_metaProvider.get().getConfigUpgradePack(
+                targetStackId.getStackName(), intermediateStack.version);
+        intermediateConfigUpgradePacks.add(intermediateConfigUpgradePack);
+      }
+      configUpgradePack = ConfigUpgradePack.merge(intermediateConfigUpgradePacks);
+    }
+
     for (UpgradeGroupHolder group : groups) {
       UpgradeGroupEntity groupEntity = new UpgradeGroupEntity();
       groupEntity.setName(group.name);
@@ -724,7 +738,7 @@ public class UpgradeResourceProvider extends AbstractControllerResourceProvider
     Map<String, Map<String, String>> newConfigurationsByType = null;
     ConfigHelper configHelper = getManagementController().getConfigHelper();
 
-    // TODO AMBARI-12698, handle jumping across several stacks and applying configs.
+    // TODO AMBARI-12698, handle jumping across several stacks
     if (direction == Direction.UPGRADE) {
       // populate a map of default configurations for the old stack (this is
       // used when determining if a property has been customized and should be

http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ConfigUpgradePack.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ConfigUpgradePack.java b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ConfigUpgradePack.java
index 2896255..f2e2e61 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ConfigUpgradePack.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/ConfigUpgradePack.java
@@ -27,6 +27,9 @@ import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementWrapper;
 import javax.xml.bind.annotation.XmlRootElement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -34,18 +37,22 @@ import java.util.Map;
 /**
  * Represents a pack of changes that should be applied to configs
  * when upgrading from a previous stack. In other words, it's a config delta
- * from prev stack
+ * from prev stack.
+ *
+ * After first call of enumerateConfigChangesByID() method, instance contains
+ * a cache of data, so it should not be modified in runtime (otherwise
+ * the cache will become outdated).
  */
 @XmlRootElement(name="upgrade-config-changes")
 @XmlAccessorType(XmlAccessType.FIELD)
 public class ConfigUpgradePack {
 
   /**
-   * Defines per-service config changes
+   * Defines per-service config changes.
    */
   @XmlElementWrapper(name="services")
   @XmlElement(name="service")
-  private List<AffectedService> services;
+  public List<AffectedService> services;
 
   /**
    * Contains a cached mapping of <change id, change definition>.
@@ -65,14 +72,6 @@ public class ConfigUpgradePack {
   }
 
   /**
-   * @return a list of per-service config changes. List should not be modified
-   * in runtime, since it will make cache stale.
-   */
-  public List<AffectedService> getServices() {
-    return services;
-  }
-
-  /**
    * @return a map of <service name, AffectedService>.
    */
   public Map<String, AffectedService> getServiceMap() {
@@ -109,6 +108,52 @@ public class ConfigUpgradePack {
   }
 
   /**
+   * Merges few config upgrade packs into one and returs result. During merge,
+   * a deep copy of AffectedService and AffectedComponent lists is added to resulting
+   * config upgrade pack. The only level that is not copied deeply is a list of
+   * per-component config changes.
+   * @param cups list of source config upgrade packs
+   * @return merged config upgrade pack that is a deep copy of source
+   * config upgrade packs
+   */
+  public static ConfigUpgradePack merge(ArrayList<ConfigUpgradePack> cups) {
+    // Map <service_name, <component_name, component_changes>>
+    Map<String, Map<String, AffectedComponent>> mergedServiceMap = new HashMap<>();
+
+    for (ConfigUpgradePack configUpgradePack : cups) {
+      for (AffectedService service : configUpgradePack.services) {
+        if (! mergedServiceMap.containsKey(service.name)) {
+          mergedServiceMap.put(service.name, new HashMap<String, AffectedComponent>());
+        }
+        Map<String, AffectedComponent> mergedComponentMap = mergedServiceMap.get(service.name);
+
+        for (AffectedComponent component : service.components) {
+          if (! mergedComponentMap.containsKey(component.name)) {
+            AffectedComponent mergedComponent = new AffectedComponent();
+            mergedComponent.name = component.name;
+            mergedComponent.changes = new ArrayList<>();
+            mergedComponentMap.put(component.name, mergedComponent);
+          }
+          AffectedComponent mergedComponent = mergedComponentMap.get(component.name);
+          mergedComponent.changes.addAll(component.changes);
+        }
+
+      }
+    }
+    // Convert merged maps into new ConfigUpgradePack
+    ArrayList<AffectedService> mergedServices = new ArrayList<>();
+    for (String serviceName : mergedServiceMap.keySet()) {
+      AffectedService mergedService = new AffectedService();
+      Map<String, AffectedComponent> mergedComponentMap = mergedServiceMap.get(serviceName);
+      mergedService.name = serviceName;
+      mergedService.components = new ArrayList<>(mergedComponentMap.values());
+      mergedServices.add(mergedService);
+    }
+
+    return new ConfigUpgradePack(mergedServices);
+  }
+
+  /**
    * A service definition in the 'services' element.
    */
   public static class AffectedService {

http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/main/java/org/apache/ambari/server/state/stack/UpgradePack.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/UpgradePack.java b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/UpgradePack.java
index 6ae0a15..aa207f1 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/stack/UpgradePack.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/stack/UpgradePack.java
@@ -60,6 +60,10 @@ public class UpgradePack {
   @XmlElement(name="type", defaultValue="rolling")
   private UpgradeType type;
 
+  @XmlElementWrapper(name="upgrade-path")
+  @XmlElement(name="intermediate-stack")
+  private List<IntermediateStack> intermediateStacks;
+
   @XmlElementWrapper(name="order")
   @XmlElement(name="group")
   private List<Grouping> groups;
@@ -120,6 +124,13 @@ public class UpgradePack {
   }
 
   /**
+   * @return a list for intermediate stacks for cross-stack upgrade, or null if no any
+   */
+  public List<IntermediateStack> getIntermediateStacks() {
+    return intermediateStacks;
+  }
+
+  /**
    * Gets the groups defined for the upgrade pack.  If a direction is defined
    * for a group, it must match the supplied direction to be returned
    * @param direction the direction to return the ordered groups
@@ -310,4 +321,14 @@ public class UpgradePack {
     @XmlElement(name="task")
     public List<Task> postDowngradeTasks;
   }
+
+  /**
+   * An intermediate stack definition in
+   * upgrade/upgrade-path/intermediate-stack path
+   */
+  public static class IntermediateStack {
+
+    @XmlAttribute
+    public String version;
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/main/resources/stacks/HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/stacks/HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml b/ambari-server/src/main/resources/stacks/HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml
index 743204c0..1da05c2 100644
--- a/ambari-server/src/main/resources/stacks/HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml
+++ b/ambari-server/src/main/resources/stacks/HDP/2.1/upgrades/nonrolling-upgrade-2.3.xml
@@ -22,6 +22,11 @@
   <target-stack>HDP-2.3</target-stack>
   <type>NON_ROLLING</type>
 
+  <upgrade-path>
+    <intermediate-stack version="2.2"/>
+    <intermediate-stack version="2.3"/>
+  </upgrade-path>
+
   <order>
     <group xsi:type="cluster" name="PRE_CLUSTER" title="Prepare Upgrade">
       <skippable>true</skippable>

http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/test/java/org/apache/ambari/server/checks/UpgradeCheckStackVersionTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/checks/UpgradeCheckStackVersionTest.java b/ambari-server/src/test/java/org/apache/ambari/server/checks/UpgradeCheckStackVersionTest.java
deleted file mode 100644
index 8d8b08f..0000000
--- a/ambari-server/src/test/java/org/apache/ambari/server/checks/UpgradeCheckStackVersionTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * 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.checks;
-
-import junit.framework.Assert;
-
-import org.apache.ambari.server.controller.PrereqCheckRequest;
-import org.apache.ambari.server.state.StackId;
-import org.easymock.EasyMock;
-import org.junit.Test;
-
-
-/**
- * Tests that the {@link AbstractCheckDescriptor} instances will return the
- * correct values for
- * {@link AbstractCheckDescriptor#isApplicable(org.apache.ambari.server.controller.PrereqCheckRequest)}
- * when different stack versions are present.
- */
-public class UpgradeCheckStackVersionTest {
-
-  @Test
-  public void testUpgradeCheckForMoreRecentStack() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(new StackId("HDP-2.3"));
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(new StackId("HDP-2.3"));
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.2.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.2"));
-
-    // false because the upgrade is for 2.2->2.2 and the check starts at 2.3
-    Assert.assertFalse(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-
-  @Test
-  public void testUpgradeCheckForOlderStack() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(new StackId("HDP-2.2"));
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(new StackId("HDP-2.2"));
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.3.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.3"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.3"));
-
-    // false because the upgrade is for 2.3->2.3 and the check is only for 2.2
-    Assert.assertFalse(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-
-  @Test
-  public void testUpgradeCheckForWithinStackOnly() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(new StackId("HDP-2.2"));
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(new StackId("HDP-2.2"));
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.3.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.3"));
-
-    // false because the upgrade is for 2.2->2.3 and the check is only for 2.2
-    // to 2.2
-    Assert.assertFalse(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-
-  @Test
-  public void testUpgradeCheckMatchesExactly() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(new StackId("HDP-2.2"));
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(new StackId("HDP-2.2"));
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.2.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.2"));
-
-    // pass because the upgrade is for 2.2->2.2 and the check is only for 2.2
-    // to 2.2
-    Assert.assertTrue(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-
-  @Test
-  public void testNoUpgradeStacksDefined() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(null);
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(null);
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.3.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.3"));
-
-    // pass because there are no restrictions
-    Assert.assertTrue(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-
-  @Test
-  public void testUpgradeStartsAtSpecifiedStackVersion() throws Exception {
-    AbstractCheckDescriptor invalidCheck = EasyMock.createMockBuilder(AbstractCheckDescriptor.class).addMockedMethods(
-        "getSourceStack", "getTargetStack").createMock();
-
-    EasyMock.expect(invalidCheck.getSourceStack()).andReturn(new StackId("HDP-2.3")).atLeastOnce();
-    EasyMock.expect(invalidCheck.getTargetStack()).andReturn(null).atLeastOnce();
-
-    EasyMock.replay(invalidCheck);
-
-    PrereqCheckRequest checkRequest = new PrereqCheckRequest("c1");
-    checkRequest.setRepositoryVersion("HDP-2.2.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.2"));
-
-    // false because this check starts at 2.3 and the upgrade is 2.2 -> 2.2
-    Assert.assertFalse(invalidCheck.isApplicable(checkRequest));
-
-    checkRequest.setRepositoryVersion("HDP-2.3.0.0");
-    checkRequest.setSourceStackId(new StackId("HDP", "2.2"));
-    checkRequest.setTargetStackId(new StackId("HDP", "2.3"));
-
-    // false because this check starts at 2.3 and the upgrade is 2.2 -> 2.3
-    Assert.assertFalse(invalidCheck.isApplicable(checkRequest));
-
-    EasyMock.verify(invalidCheck);
-  }
-}

http://git-wip-us.apache.org/repos/asf/ambari/blob/b9b708e6/ambari-server/src/test/java/org/apache/ambari/server/state/stack/ConfigUpgradePackTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/stack/ConfigUpgradePackTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/stack/ConfigUpgradePackTest.java
new file mode 100644
index 0000000..da5c0ab
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/stack/ConfigUpgradePackTest.java
@@ -0,0 +1,148 @@
+/**
+ * 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.state.stack;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.persist.PersistService;
+import org.apache.ambari.server.api.services.AmbariMetaInfo;
+import org.apache.ambari.server.orm.GuiceJpaInitializer;
+import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
+import org.apache.ambari.server.state.stack.upgrade.ClusterGrouping;
+import org.apache.ambari.server.state.stack.upgrade.ClusterGrouping.ExecuteStage;
+import org.apache.ambari.server.state.stack.upgrade.ConfigUpgradeChangeDefinition;
+import org.apache.ambari.server.state.stack.upgrade.Direction;
+import org.apache.ambari.server.state.stack.upgrade.Grouping;
+import org.apache.ambari.server.state.stack.upgrade.RestartGrouping;
+import org.apache.ambari.server.state.stack.upgrade.StopGrouping;
+import org.apache.ambari.server.state.stack.upgrade.UpgradeType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import static org.apache.ambari.server.state.stack.ConfigUpgradePack.AffectedService;
+import static org.apache.ambari.server.state.stack.ConfigUpgradePack.AffectedComponent;
+
+/**
+ * Tests for the config upgrade pack
+ */
+public class ConfigUpgradePackTest {
+
+  @Test
+  public void testMerge() {
+
+    // Generate test data - 3 config upgrade packs, 2 services, 2 components, 2 config changes each
+    ArrayList<ConfigUpgradePack> cups = new ArrayList<>();
+    for (int cupIndex = 0; cupIndex < 3; cupIndex++) {
+
+      ArrayList<AffectedService> services = new ArrayList<>();
+      for (int serviceIndex = 0; serviceIndex < 2; serviceIndex++) {
+        String serviceName;
+        if (serviceIndex == 0) {
+          serviceName = "HDFS";  // For checking merge of existing services
+        } else {
+          serviceName = String.format("SOME_SERVICE_%s", cupIndex);
+        }
+        ArrayList<AffectedComponent> components = new ArrayList<>();
+        for (int componentIndex = 0; componentIndex < 2; componentIndex++) {
+          String componentName;
+          if (componentIndex == 0) {
+            componentName = "NAMENODE";  // For checking merge of existing components
+          } else {
+            componentName = "SOME_COMPONENT_" + cupIndex;
+          }
+
+          ArrayList<ConfigUpgradeChangeDefinition> changeDefinitions = new ArrayList<>();
+          for (int changeIndex = 0; changeIndex < 2; changeIndex++) {
+            String change_id = String.format(
+                    "CHANGE_%s_%s_%s_%s", cupIndex, serviceIndex, componentIndex, changeIndex);
+            ConfigUpgradeChangeDefinition changeDefinition = new ConfigUpgradeChangeDefinition();
+            changeDefinition.id = change_id;
+            changeDefinitions.add(changeDefinition);
+          }
+          AffectedComponent component = new AffectedComponent();
+          component.name = componentName;
+          component.changes = changeDefinitions;
+          components.add(component);
+        }
+        AffectedService service = new AffectedService();
+        service.name = serviceName;
+        service.components = components;
+        services.add(service);
+      }
+      ConfigUpgradePack cupI = new ConfigUpgradePack();
+      cupI.services = services;
+      cups.add(cupI);
+    }
+
+    // Merge
+
+    ConfigUpgradePack result = ConfigUpgradePack.merge(cups);
+
+
+    // Check test results
+
+    assertEquals(result.enumerateConfigChangesByID().entrySet().size(), 24);
+
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(0).id, "CHANGE_0_0_0_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(1).id, "CHANGE_0_0_0_1");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(2).id, "CHANGE_1_0_0_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(3).id, "CHANGE_1_0_0_1");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(4).id, "CHANGE_2_0_0_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("NAMENODE").changes.get(5).id, "CHANGE_2_0_0_1");
+
+
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_0").changes.get(0).id, "CHANGE_0_0_1_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_0").changes.get(1).id, "CHANGE_0_0_1_1");
+
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_1").changes.get(0).id, "CHANGE_1_0_1_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_1").changes.get(1).id, "CHANGE_1_0_1_1");
+
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_2").changes.get(0).id, "CHANGE_2_0_1_0");
+    assertEquals(result.getServiceMap().get("HDFS").getComponentMap().get("SOME_COMPONENT_2").changes.get(1).id, "CHANGE_2_0_1_1");
+
+
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_0").getComponentMap().get("NAMENODE").changes.get(0).id, "CHANGE_0_1_0_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_0").getComponentMap().get("NAMENODE").changes.get(1).id, "CHANGE_0_1_0_1");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_0").getComponentMap().get("SOME_COMPONENT_0").changes.get(0).id, "CHANGE_0_1_1_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_0").getComponentMap().get("SOME_COMPONENT_0").changes.get(1).id, "CHANGE_0_1_1_1");
+
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_1").getComponentMap().get("NAMENODE").changes.get(0).id, "CHANGE_1_1_0_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_1").getComponentMap().get("NAMENODE").changes.get(1).id, "CHANGE_1_1_0_1");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_1").getComponentMap().get("SOME_COMPONENT_1").changes.get(0).id, "CHANGE_1_1_1_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_1").getComponentMap().get("SOME_COMPONENT_1").changes.get(1).id, "CHANGE_1_1_1_1");
+
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_2").getComponentMap().get("NAMENODE").changes.get(0).id, "CHANGE_2_1_0_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_2").getComponentMap().get("NAMENODE").changes.get(1).id, "CHANGE_2_1_0_1");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_2").getComponentMap().get("SOME_COMPONENT_2").changes.get(0).id, "CHANGE_2_1_1_0");
+    assertEquals(result.getServiceMap().get("SOME_SERVICE_2").getComponentMap().get("SOME_COMPONENT_2").changes.get(1).id, "CHANGE_2_1_1_1");
+
+  }
+
+}