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 2015/05/28 04:39:22 UTC

ambari git commit: AMBARI-11438 - RU: Storm actions require Zookeeper host (jonathanhurley)

Repository: ambari
Updated Branches:
  refs/heads/trunk 38156eafd -> 8dd29ced1


AMBARI-11438 - RU: Storm actions require Zookeeper host (jonathanhurley)


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

Branch: refs/heads/trunk
Commit: 8dd29ced1f8bc3e96e12ed76dbe500c47361d94c
Parents: 38156ea
Author: Jonathan Hurley <jh...@hortonworks.com>
Authored: Wed May 27 14:07:33 2015 -0400
Committer: Jonathan Hurley <jh...@hortonworks.com>
Committed: Wed May 27 22:39:16 2015 -0400

----------------------------------------------------------------------
 .../main/python/ambari_commons/yaml_utils.py    | 68 ++++++++++++++++
 .../orm/entities/ClusterConfigEntity.java       |  4 +-
 .../apache/ambari/server/state/ConfigImpl.java  | 28 +++++--
 .../0.9.1.2.1/package/scripts/params_linux.py   |  3 +-
 .../STORM/0.9.1.2.1/package/scripts/storm.py    |  2 +-
 .../0.9.1.2.1/package/scripts/storm_upgrade.py  | 40 ++++++++--
 .../package/scripts/storm_yaml_utils.py         | 53 +++++++++++++
 .../0.9.1.2.1/package/scripts/yaml_utils.py     | 81 --------------------
 .../server/state/cluster/ClusterTest.java       | 61 ++++++++++++++-
 ambari-server/src/test/python/TestYAMLUtils.py  | 44 +++++++++++
 .../python/stacks/2.1/STORM/test_storm_base.py  |  4 +-
 .../python/stacks/2.3/STORM/test_storm_base.py  |  4 +-
 12 files changed, 288 insertions(+), 104 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-common/src/main/python/ambari_commons/yaml_utils.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/ambari_commons/yaml_utils.py b/ambari-common/src/main/python/ambari_commons/yaml_utils.py
new file mode 100644
index 0000000..bb05c8a
--- /dev/null
+++ b/ambari-common/src/main/python/ambari_commons/yaml_utils.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+"""
+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.
+
+"""
+import re
+
+def escape_yaml_property(value):
+  unquouted = False
+  unquouted_values = ["null", "Null", "NULL", "true", "True", "TRUE", "false",
+    "False", "FALSE", "YES", "Yes", "yes", "NO", "No", "no", "ON", "On", "on",
+    "OFF", "Off", "off"]
+
+  if value in unquouted_values:
+    unquouted = True
+
+  # if is list [a,b,c] or dictionary {a: v, b: v2, c: v3}
+  if re.match('^\w*\[.+\]\w*$', value) or re.match('^\w*\{.+\}\w*$', value):
+    unquouted = True
+
+  try:
+    int(value)
+    unquouted = True
+  except ValueError:
+    pass
+
+  try:
+    float(value)
+    unquouted = True
+  except ValueError:
+    pass
+
+  if not unquouted:
+    value = value.replace("'", "''")
+    value = "'" + value + "'"
+
+  return value
+
+def get_values_from_yaml_array(yaml_array):
+  """
+  Converts a YAML array into a normal array of values. For example, this
+  will turn ['c6401','c6402']
+  into an array with two strings, "c6401" and "c6402".
+  :param yaml_array: the YAML array to convert
+  :return:  a python array or None if the value could not be converted correctly.
+  """
+  if yaml_array is None:
+    return None
+
+  matches = re.findall(r'[\'|\"](.+?)[\'|\"]', yaml_array)
+  if matches is None or len(matches) == 0:
+    return None
+
+  return matches

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java
index 8ca2278..899ffe8 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/ClusterConfigEntity.java
@@ -72,12 +72,12 @@ public class ClusterConfigEntity {
   private String tag;
 
   @Basic(fetch = FetchType.LAZY)
-  @Column(name = "config_data", nullable = false, insertable = true, updatable = false)
+  @Column(name = "config_data", nullable = false, insertable = true)
   @Lob
   private String configJson;
 
   @Basic(fetch = FetchType.LAZY)
-  @Column(name = "config_attributes", nullable = true, insertable = true, updatable = false)
+  @Column(name = "config_attributes", nullable = true, insertable = true)
   @Lob
   private String configAttributesJson;
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigImpl.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigImpl.java
index e755f76..1543ad7 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigImpl.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigImpl.java
@@ -28,6 +28,8 @@ import org.apache.ambari.server.orm.dao.ClusterDAO;
 import org.apache.ambari.server.orm.dao.ServiceConfigDAO;
 import org.apache.ambari.server.orm.entities.ClusterConfigEntity;
 import org.apache.ambari.server.orm.entities.ClusterEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
 import com.google.inject.Inject;
@@ -37,6 +39,11 @@ import com.google.inject.assistedinject.AssistedInject;
 import com.google.inject.persist.Transactional;
 
 public class ConfigImpl implements Config {
+  /**
+   * Logger.
+   */
+  private final static Logger LOG = LoggerFactory.getLogger(ConfigImpl.class);
+
   public static final String GENERATED_TAG_PREFIX = "generatedTag_";
 
   private Cluster cluster;
@@ -47,12 +54,16 @@ public class ConfigImpl implements Config {
   private Map<String, String> properties;
   private Map<String, Map<String, String>> propertiesAttributes;
   private ClusterConfigEntity entity;
+
   @Inject
   private ClusterDAO clusterDAO;
+
   @Inject
   private Gson gson;
+
   @Inject
   private ServiceConfigDAO serviceConfigDAO;
+
   @AssistedInject
   public ConfigImpl(@Assisted Cluster cluster, @Assisted String type, @Assisted Map<String, String> properties,
       @Assisted Map<String, Map<String, String>> propertiesAttributes, Injector injector) {
@@ -66,7 +77,6 @@ public class ConfigImpl implements Config {
     stackId = cluster.getDesiredStackVersion();
 
     injector.injectMembers(this);
-
   }
 
 
@@ -204,16 +214,20 @@ public class ConfigImpl implements Config {
       entity.setTimestamp(new Date().getTime());
       entity.setStack(clusterEntity.getDesiredStack());
       entity.setData(gson.toJson(getProperties()));
+
       if (null != getPropertiesAttributes()) {
         entity.setAttributes(gson.toJson(getPropertiesAttributes()));
       }
 
       clusterDAO.createConfig(entity);
-
       clusterEntity.getClusterConfigEntities().add(entity);
+      clusterDAO.merge(clusterEntity);
+      cluster.refresh();
     } else {
       // only supporting changes to the properties
       ClusterConfigEntity entity = null;
+
+      // find the existing configuration to update
       for (ClusterConfigEntity cfe : clusterEntity.getClusterConfigEntities()) {
         if (getTag().equals(cfe.getTag()) &&
             getType().equals(cfe.getType()) &&
@@ -221,15 +235,17 @@ public class ConfigImpl implements Config {
           entity = cfe;
           break;
         }
-
       }
 
+      // if the configuration was found, then update it
       if (null != entity) {
+        LOG.debug(
+            "Updating {} version {} with new configurations; a new version will not be created",
+            getType(), getVersion());
+
         entity.setData(gson.toJson(getProperties()));
+        clusterDAO.merge(clusterEntity);
       }
     }
-
-    clusterDAO.merge(clusterEntity);
-    cluster.refresh();
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/params_linux.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/params_linux.py b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/params_linux.py
index 5ae7170..2846919 100644
--- a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/params_linux.py
+++ b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/params_linux.py
@@ -83,7 +83,8 @@ java64_home = config['hostLevelParams']['java_home']
 jps_binary = format("{java64_home}/bin/jps")
 nimbus_port = config['configurations']['storm-site']['nimbus.thrift.port']
 storm_zookeeper_root_dir = default('/configurations/storm-site/storm.zookeeper.root', None)
-storm_zookeeper_servers = default('/configurations/storm-site/storm.zookeeper.servers', None)
+storm_zookeeper_servers = config['configurations']['storm-site']['storm.zookeeper.servers']
+storm_zookeeper_port = config['configurations']['storm-site']['storm.zookeeper.port']
 
 # nimbus.seeds is supported in HDP 2.3.0.0 and higher
 nimbus_seeds_supported = default('/configurations/storm-env/nimbus_seeds_supported', False)

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm.py b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm.py
index e7e5a4c..8d629ab 100644
--- a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm.py
+++ b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm.py
@@ -27,7 +27,7 @@ from resource_management.libraries.functions.format import format
 from resource_management.libraries.script.script import Script
 from resource_management.core.source import Template
 from resource_management.libraries.functions import compare_versions
-from yaml_utils import yaml_config_template, yaml_config
+from storm_yaml_utils import yaml_config_template, yaml_config
 from ambari_commons.os_family_impl import OsFamilyFuncImpl, OsFamilyImpl
 from ambari_commons import OSConst
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_upgrade.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_upgrade.py b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_upgrade.py
index b25cdf8..3f87694 100644
--- a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_upgrade.py
+++ b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_upgrade.py
@@ -19,6 +19,7 @@ limitations under the License.
 import json
 import os
 
+from ambari_commons import yaml_utils
 from resource_management.core.logger import Logger
 from resource_management.core.exceptions import Fail
 from resource_management.core.resources.system import Directory
@@ -48,24 +49,49 @@ class StormUpgrade(Script):
     """
     import params
 
+
     Logger.info('Clearing Storm data from ZooKeeper')
 
     storm_zookeeper_root_dir = params.storm_zookeeper_root_dir
     if storm_zookeeper_root_dir is None:
       raise Fail("The storm ZooKeeper directory specified by storm-site/storm.zookeeper.root must be specified")
 
-    # create the ZooKeeper delete command
-    if params.version is not None:
-      command = "/usr/hdp/{0}/zookeeper/bin/zkCli.sh rmr /storm".format(params.version)
-    else:
-      command = "/usr/hdp/current/zookeeper-client/bin/zkCli.sh rmr /storm"
+    # the zookeeper client must be given a zookeeper host to contact
+    storm_zookeeper_server_list = yaml_utils.get_values_from_yaml_array(params.storm_zookeeper_servers)
+    if storm_zookeeper_server_list is None:
+      Logger.info("Unable to extract ZooKeeper hosts from '{0}', assuming localhost").format(params.storm_zookeeper_servers)
+      storm_zookeeper_server_list = ["localhost"]
 
+    # kinit if needed
     if params.security_enabled:
       kinit_command=format("{kinit_path_local} -kt {smoke_user_keytab} {smokeuser_principal}; ")
       Execute(kinit_command,user=params.smokeuser)
 
-    # clean out ZK
-    Execute(command, user=params.storm_user, tries=1)
+    # for every zk server, try to remove /storm
+    zookeeper_data_cleared = False
+    for storm_zookeeper_server in storm_zookeeper_server_list:
+      # determine where the zkCli.sh shell script is
+      zk_command_location = "/usr/hdp/current/zookeeper-client/bin/zkCli.sh"
+      if params.version is not None:
+        zk_command_location = "/usr/hdp/{0}/zookeeper/bin/zkCli.sh".format(params.version)
+
+      # create the ZooKeeper delete command
+      command = "{0} -server {1}:{2} rmr /storm".format(
+        zk_command_location, storm_zookeeper_server, params.storm_zookeeper_port)
+
+      # clean out ZK
+      try:
+        Execute(command, user=params.storm_user, logoutput=True, tries=1)
+        zookeeper_data_cleared = True
+        break
+      except:
+        # the command failed, try a different ZK server
+        pass
+
+    # fail if the ZK data could not be cleared
+    if not zookeeper_data_cleared:
+      raise Fail("Unable to clear ZooKeeper Storm data on any of the following ZooKeeper hosts: {0}".format(
+        storm_zookeeper_server_list))
 
 
   def delete_storm_local_data(self, env):

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_yaml_utils.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_yaml_utils.py b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_yaml_utils.py
new file mode 100644
index 0000000..9d78e71
--- /dev/null
+++ b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/storm_yaml_utils.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+"""
+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.
+
+"""
+import os
+import resource_management
+
+from ambari_commons.yaml_utils import escape_yaml_property
+from resource_management.core.source import InlineTemplate
+from resource_management.core.resources.system import File
+
+def replace_jaas_placeholder(name, security_enabled, conf_dir):
+  if name.find('_JAAS_PLACEHOLDER') > -1:
+    if security_enabled:
+      return name.replace('_JAAS_PLACEHOLDER', '-Djava.security.auth.login.config=' + conf_dir + '/storm_jaas.conf')
+    else:
+      return name.replace('_JAAS_PLACEHOLDER', '')
+  else:
+    return name
+
+storm_yaml_template = """{% for key, value in configurations|dictsort if not key.startswith('_') %}{{key}} : {{ escape_yaml_property(replace_jaas_placeholder(resource_management.core.source.InlineTemplate(value).get_content().strip(), security_enabled, conf_dir)) }}
+{% endfor %}"""
+
+def yaml_config_template(configurations):
+  return InlineTemplate(storm_yaml_template, configurations=configurations,
+                        extra_imports=[escape_yaml_property, replace_jaas_placeholder, resource_management,
+                                       resource_management.core, resource_management.core.source])
+
+def yaml_config(filename, configurations = None, conf_dir = None, owner = None, group = None):
+  import params
+  config_content = InlineTemplate('''{% for key, value in configurations_dict|dictsort %}{{ key }}: {{ escape_yaml_property(resource_management.core.source.InlineTemplate(value).get_content()) }}
+{% endfor %}''', configurations_dict=configurations, extra_imports=[escape_yaml_property, resource_management, resource_management.core, resource_management.core.source])
+
+  File (os.path.join(params.conf_dir, filename),
+        content = config_content,
+        owner = owner,
+        mode = "f"
+  )

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/yaml_utils.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/yaml_utils.py b/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/yaml_utils.py
deleted file mode 100644
index 7b71553..0000000
--- a/ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/package/scripts/yaml_utils.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python
-"""
-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.
-
-"""
-
-import re
-import os
-import resource_management
-from resource_management.core.source import InlineTemplate
-from resource_management.core.resources.system import File
-
-def escape_yaml_property(value):
-  unquouted = False
-  unquouted_values = ["null","Null","NULL","true","True","TRUE","false","False","FALSE","YES","Yes","yes","NO","No","no","ON","On","on","OFF","Off","off"]
-  if value in unquouted_values:
-    unquouted = True
-
-  # if is list [a,b,c] or dictionary {a: v, b: v2, c: v3}
-  if re.match('^\w*\[.+\]\w*$', value) or re.match('^\w*\{.+\}\w*$', value):
-    unquouted = True
-
-  try:
-    int(value)
-    unquouted = True
-  except ValueError:
-    pass
-  
-  try:
-    float(value)
-    unquouted = True
-  except ValueError:
-    pass
-  
-  if not unquouted:
-    value = value.replace("'","''")
-    value = "'"+value+"'"
-    
-  return value
-
-def replace_jaas_placeholder(name, security_enabled, conf_dir):
-  if name.find('_JAAS_PLACEHOLDER') > -1:
-    if security_enabled:
-      return name.replace('_JAAS_PLACEHOLDER', '-Djava.security.auth.login.config=' + conf_dir + '/storm_jaas.conf')
-    else:
-      return name.replace('_JAAS_PLACEHOLDER', '')
-  else:
-    return name
-
-storm_yaml_template = """{% for key, value in configurations|dictsort if not key.startswith('_') %}{{key}} : {{ escape_yaml_property(replace_jaas_placeholder(resource_management.core.source.InlineTemplate(value).get_content().strip(), security_enabled, conf_dir)) }}
-{% endfor %}"""
-
-def yaml_config_template(configurations):
-  return InlineTemplate(storm_yaml_template, configurations=configurations,
-                        extra_imports=[escape_yaml_property, replace_jaas_placeholder, resource_management,
-                                       resource_management.core, resource_management.core.source])
-
-def yaml_config(filename, configurations = None, conf_dir = None, owner = None, group = None):
-  import params
-  config_content = InlineTemplate('''{% for key, value in configurations_dict|dictsort %}{{ key }}: {{ escape_yaml_property(resource_management.core.source.InlineTemplate(value).get_content()) }}
-{% endfor %}''', configurations_dict=configurations, extra_imports=[escape_yaml_property, resource_management, resource_management.core, resource_management.core.source])
-
-  File (os.path.join(params.conf_dir, filename),
-        content = config_content,
-        owner = owner,
-        mode = "f"
-  )
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterTest.java
index 380b3bd..710c046 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterTest.java
@@ -42,8 +42,6 @@ import java.util.Set;
 import javax.persistence.EntityManager;
 import javax.persistence.RollbackException;
 
-import com.google.gson.Gson;
-import com.google.inject.persist.UnitOfWork;
 import junit.framework.Assert;
 
 import org.apache.ambari.server.AmbariException;
@@ -65,6 +63,7 @@ import org.apache.ambari.server.orm.dao.HostVersionDAO;
 import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
 import org.apache.ambari.server.orm.dao.ResourceTypeDAO;
 import org.apache.ambari.server.orm.dao.StackDAO;
+import org.apache.ambari.server.orm.entities.ClusterConfigEntity;
 import org.apache.ambari.server.orm.entities.ClusterEntity;
 import org.apache.ambari.server.orm.entities.ClusterServiceEntity;
 import org.apache.ambari.server.orm.entities.ClusterStateEntity;
@@ -108,12 +107,14 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
+import com.google.gson.Gson;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 import com.google.inject.persist.PersistService;
 import com.google.inject.persist.Transactional;
+import com.google.inject.persist.UnitOfWork;
 import com.google.inject.util.Modules;
 
 public class ClusterTest {
@@ -1905,4 +1906,60 @@ public class ClusterTest {
     verify(hostVersionDAOMock).merge(hostVersionCaptor.capture());
     assertEquals(hostVersionCaptor.getValue().getState(), RepositoryVersionState.CURRENT);
   }
+
+  /**
+   * Tests that an existing configuration can be successfully updated without
+   * creating a new version.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testClusterConfigMergingWithoutNewVersion() throws Exception {
+    createDefaultCluster();
+
+    Cluster cluster = clusters.getCluster("c1");
+    ClusterEntity clusterEntity = clusterDAO.findByName("c1");
+    assertEquals(0, clusterEntity.getClusterConfigEntities().size());
+
+    final Config originalConfig = configFactory.createNew(cluster, "foo-site",
+        new HashMap<String, String>() {
+          {
+            put("one", "two");
+          }
+        }, new HashMap<String, Map<String, String>>());
+
+    originalConfig.setTag("version3");
+    originalConfig.persist();
+    cluster.addConfig(originalConfig);
+
+    ConfigGroup configGroup = configGroupFactory.createNew(cluster, "g1", "t1", "",
+        new HashMap<String, Config>() {
+          {
+            put("foo-site", originalConfig);
+          }
+        }, Collections.<Long, Host> emptyMap());
+
+    configGroup.persist();
+    cluster.addConfigGroup(configGroup);
+
+    clusterEntity = clusterDAO.findByName("c1");
+    assertEquals(1, clusterEntity.getClusterConfigEntities().size());
+
+    Map<String, Config> configsByType = cluster.getConfigsByType("foo-site");
+    Config config = configsByType.entrySet().iterator().next().getValue();
+
+    Map<String, String> properties = config.getProperties();
+    properties.put("three", "four");
+    config.setProperties(properties);
+
+    config.persist(false);
+
+    clusterEntity = clusterDAO.findByName("c1");
+    assertEquals(1, clusterEntity.getClusterConfigEntities().size());
+    ClusterConfigEntity clusterConfigEntity = clusterEntity.getClusterConfigEntities().iterator().next();
+    assertTrue(clusterConfigEntity.getData().contains("one"));
+    assertTrue(clusterConfigEntity.getData().contains("two"));
+    assertTrue(clusterConfigEntity.getData().contains("three"));
+    assertTrue(clusterConfigEntity.getData().contains("four"));
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/test/python/TestYAMLUtils.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/python/TestYAMLUtils.py b/ambari-server/src/test/python/TestYAMLUtils.py
new file mode 100644
index 0000000..bdbb11f
--- /dev/null
+++ b/ambari-server/src/test/python/TestYAMLUtils.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+'''
+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.
+'''
+
+from unittest import TestCase
+
+from ambari_commons import yaml_utils
+
+class TestYAMLUtils(TestCase):
+  def setUp(self):
+    pass
+
+  def test_convert_yaml_array(self):
+    expected_values = []
+
+    expected_values.append("c6401.ambari.apache.org")
+    values = yaml_utils.get_values_from_yaml_array("['c6401.ambari.apache.org']")
+    self.assertEquals(expected_values, values)
+
+    expected_values.append("c6402.ambari.apache.org")
+    values = yaml_utils.get_values_from_yaml_array("['c6401.ambari.apache.org', 'c6402.ambari.apache.org']")
+    self.assertEquals(expected_values, values)
+
+    values = yaml_utils.get_values_from_yaml_array('["c6401.ambari.apache.org", "c6402.ambari.apache.org"]')
+    self.assertEquals(expected_values, values)
+
+    values = yaml_utils.get_values_from_yaml_array('[\'c6401.ambari.apache.org\', "c6402.ambari.apache.org"]')
+    self.assertEquals(expected_values, values)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/test/python/stacks/2.1/STORM/test_storm_base.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/python/stacks/2.1/STORM/test_storm_base.py b/ambari-server/src/test/python/stacks/2.1/STORM/test_storm_base.py
index 8d82f2a..daf4bfe 100644
--- a/ambari-server/src/test/python/stacks/2.1/STORM/test_storm_base.py
+++ b/ambari-server/src/test/python/stacks/2.1/STORM/test_storm_base.py
@@ -113,10 +113,10 @@ class TestStormBase(RMFTestCase):
     return storm_yarn_content
 
   def call_storm_template_and_assert(self, confDir="/etc/storm/conf"):
-    import yaml_utils
+    import storm_yaml_utils
 
     with RMFTestCase.env as env:
-      storm_yarn_temlate = yaml_utils.yaml_config_template(self.getConfig()['configurations']['storm-site'])
+      storm_yarn_temlate = storm_yaml_utils.yaml_config_template(self.getConfig()['configurations']['storm-site'])
 
       self.assertResourceCalled('File', confDir + '/storm.yaml',
         owner = 'storm',

http://git-wip-us.apache.org/repos/asf/ambari/blob/8dd29ced/ambari-server/src/test/python/stacks/2.3/STORM/test_storm_base.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/python/stacks/2.3/STORM/test_storm_base.py b/ambari-server/src/test/python/stacks/2.3/STORM/test_storm_base.py
index e0a050f..5033bff 100644
--- a/ambari-server/src/test/python/stacks/2.3/STORM/test_storm_base.py
+++ b/ambari-server/src/test/python/stacks/2.3/STORM/test_storm_base.py
@@ -110,10 +110,10 @@ class TestStormBase(RMFTestCase):
     return storm_yarn_content
 
   def call_storm_template_and_assert(self, confDir="/etc/storm/conf"):
-    import yaml_utils
+    import storm_yaml_utils
 
     with RMFTestCase.env as env:
-      storm_yarn_temlate = yaml_utils.yaml_config_template(self.getConfig()['configurations']['storm-site'])
+      storm_yarn_temlate = storm_yaml_utils.yaml_config_template(self.getConfig()['configurations']['storm-site'])
 
       self.assertResourceCalled('File', confDir + '/storm.yaml',
         owner = 'storm',