You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by pa...@apache.org on 2021/01/25 09:38:51 UTC

[ambari] branch branch-2.7 updated: AMBARI-25547 Update Grafana version to 6.7.4 to avoid CVE-2020-13379 (#3279)

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

payert pushed a commit to branch branch-2.7
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/branch-2.7 by this push:
     new c45bf00  AMBARI-25547 Update Grafana version to 6.7.4 to avoid CVE-2020-13379 (#3279)
c45bf00 is described below

commit c45bf00abd369342d495f1a8313eda7075441c40
Author: Tamas Payer <35...@users.noreply.github.com>
AuthorDate: Mon Jan 25 10:38:38 2021 +0100

    AMBARI-25547 Update Grafana version to 6.7.4 to avoid CVE-2020-13379 (#3279)
    
    * Change Grafana versinon to 6.7.4
    * Update ams-grafana-ini template upon Ambari upgrade
    * Fix string util functions and add unit test
    * Fix typos in ams-grafana-ini.xml
---
 ambari-metrics/pom.xml                             |   4 +-
 .../ambari/server/upgrade/UpgradeCatalog275.java   | 181 +++++++++++++++++++++
 .../ambari/server/utils/CustomStringUtils.java     | 103 ++++++++++++
 .../0.2.0/configuration/ams-grafana-ini.xml        |   6 +-
 .../ambari/server/utils/CustomStringUtilsTest.java |  96 +++++++++++
 5 files changed, 385 insertions(+), 5 deletions(-)

diff --git a/ambari-metrics/pom.xml b/ambari-metrics/pom.xml
index 5fa9c3d..e828b03 100644
--- a/ambari-metrics/pom.xml
+++ b/ambari-metrics/pom.xml
@@ -44,8 +44,8 @@
     <hbase.folder>hbase-2.0.2.3.1.4.1-1</hbase.folder>
     <hadoop.tar>https://private-repo-1.hortonworks.com/HDP/centos7/3.x/updates/3.1.4.1-1/tars/hadoop/hadoop-3.1.1.3.1.4.1-1.tar.gz</hadoop.tar>
     <hadoop.folder>hadoop-3.1.1.3.1.4.1-1</hadoop.folder>
-    <grafana.folder>grafana-6.4.2</grafana.folder>
-    <grafana.tar>https://dl.grafana.com/oss/release/grafana-6.4.2.linux-amd64.tar.gz</grafana.tar>
+    <grafana.folder>grafana-6.7.4</grafana.folder>
+    <grafana.tar>https://dl.grafana.com/oss/release/grafana-6.7.4.linux-amd64.tar.gz</grafana.tar>
     <phoenix.tar>https://private-repo-1.hortonworks.com/HDP/centos7/3.x/updates/3.1.4.1-1/tars/phoenix/phoenix-5.0.0.3.1.4.1-1.tar.gz</phoenix.tar>
     <phoenix.folder>phoenix-5.0.0.3.1.4.1-1</phoenix.folder>
     <resmonitor.install.dir>/usr/lib/python2.6/site-packages/resource_monitoring</resmonitor.install.dir>
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog275.java b/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog275.java
index c369b38..c9a2dd9 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog275.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/upgrade/UpgradeCatalog275.java
@@ -17,16 +17,28 @@
  */
 package org.apache.ambari.server.upgrade;
 
+import static org.apache.ambari.server.utils.CustomStringUtils.deleteSubstring;
+import static org.apache.ambari.server.utils.CustomStringUtils.insertAfterIfNotThere;
+import static org.apache.ambari.server.utils.CustomStringUtils.replace;
+import static org.apache.ambari.server.utils.CustomStringUtils.replaceIfNotThere;
+
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.orm.dao.BlueprintDAO;
 import org.apache.ambari.server.orm.entities.BlueprintConfigEntity;
 import org.apache.ambari.server.orm.entities.BlueprintEntity;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.Config;
+import org.apache.commons.collections.MapUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -91,6 +103,7 @@ public class UpgradeCatalog275 extends AbstractUpgradeCatalog {
   protected void executeDMLUpdates() throws AmbariException, SQLException {
     LOG.debug("UpgradeCatalog275 executing DML Updates.");
     addNewConfigurationsFromXml();
+    updateAmsGrafanaIniConfig();
   }
 
   protected void removeDfsHAInitial() {
@@ -122,4 +135,172 @@ public class UpgradeCatalog275 extends AbstractUpgradeCatalog {
       blueprintDAO.merge(blueprintEntity);
     }
   }
+
+  protected void updateAmsGrafanaIniConfig() throws AmbariException {
+    AmbariManagementController ambariManagementController = injector.getInstance(AmbariManagementController.class);
+    Clusters clusters = ambariManagementController.getClusters();
+    if (clusters != null) {
+      Map<String, Cluster> clusterMap = getCheckedClusterMap(clusters);
+      if (MapUtils.isNotEmpty(clusterMap)) {
+        for (Cluster cluster : clusterMap.values()) {
+          Set<String> installedServices = cluster.getServices().keySet();
+          if (installedServices.contains("AMBARI_METRICS")) {
+            Config amsGrafanaIniConf = cluster.getDesiredConfigByType("ams-grafana-ini");
+            if (amsGrafanaIniConf != null) {
+              String contentText = amsGrafanaIniConf.getProperties().get("content");
+              if (contentText != null) {
+                String addAfter;
+                String toInsert;
+                String toFind;
+                String toReplace;
+                StringBuilder content = new StringBuilder(contentText);
+
+                addAfter = "; app_mode = production";
+                toInsert = "\n" +
+                    "\n# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty" +
+                    "\n; instance_name = ${HOSTNAME}";
+                insertAfterIfNotThere(content, addAfter, toInsert);
+
+                addAfter = "logs = {{ams_grafana_log_dir}}";
+                String pluginsConfLine = "plugins = /var/lib/ambari-metrics-grafana/plugins";
+                toInsert = "\n" +
+                    "\n# Directory where grafana will automatically scan and look for plugins" +
+                    "\n" + pluginsConfLine;
+                insertAfterIfNotThere(content, addAfter, toInsert, pluginsConfLine);
+
+                deleteSubstring(content, ";protocol = http\n");
+                deleteSubstring(content, ";http_port = 3000\n");
+                deleteSubstring(content, ";static_root_path = public\n");
+                deleteSubstring(content, ";cert_file =\n");
+                deleteSubstring(content, ";cert_key =\n");
+
+                addAfter = "cert_key = {{ams_grafana_cert_key}}";
+                toInsert = "\n" +
+                    "\n# Unix socket path" +
+                    "\n;socket =";
+                insertAfterIfNotThere(content, addAfter, toInsert);
+
+                toFind = ";password =";
+                toReplace = "# If the password contains # or ; you have to wrap it with triple quotes. Ex \"\"\"#password;\"\"\"" +
+                    "\n;password =" +
+                    "\n" +
+                    "\n# Use either URL or the previous fields to configure the database" +
+                    "\n# Example: mysql://user:secret@host:port/database" +
+                    "\n;url =";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                addAfter = ";session_life_time = 86400";
+                toInsert = "\n" +
+                    "\n#################################### Data proxy ###########################" +
+                    "\n[dataproxy]" +
+                    "\n" +
+                    "\n# This enables data proxy logging, default is false" +
+                    "\n;logging = false";
+                insertAfterIfNotThere(content, addAfter, toInsert);
+
+                toFind = "# Google Analytics universal tracking code, only enabled if you specify an id here";
+                toReplace = "# Set to false to disable all checks to https://grafana.net" +
+                    "\n# for new versions (grafana itself and plugins), check is used" +
+                    "\n# in some UI views to notify that grafana or plugin update exists" +
+                    "\n# This option does not cause any auto updates, nor send any information" +
+                    "\n# only a GET request to http://grafana.com to get latest versions" +
+                    "\n;check_for_updates = true" +
+                    "\n" +
+                    "\n# Google Analytics universal tracking code, only enabled if you specify an id here";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = "#################################### Users ####################################";
+                toReplace = "[snapshots]" +
+                    "\n# snapshot sharing options" +
+                    "\n;external_enabled = true" +
+                    "\n;external_snapshot_url = https://snapshots-origin.raintank.io" +
+                    "\n;external_snapshot_name = Publish to snapshot.raintank.io" +
+                    "\n" +
+                    "\n# remove expired snapshot" +
+                    "\n;snapshot_remove_expired = true" +
+                    "\n" +
+                    "\n# remove snapshots after 90 days" +
+                    "\n;snapshot_TTL_days = 90" +
+                    "\n" +
+                    "\n#################################### Users ####################################";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = "#################################### Anonymous Auth ##########################";
+                toReplace = "# Default UI theme (\"dark\" or \"light\")" +
+                    "\n;default_theme = dark" +
+                    "\n" +
+                    "\n# External user management, these options affect the organization users view" +
+                    "\n;external_manage_link_url =" +
+                    "\n;external_manage_link_name =" +
+                    "\n;external_manage_info =" +
+                    "\n" +
+                    "\n[auth]" +
+                    "\n# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false" +
+                    "\n;disable_login_form = false" +
+                    "\n" +
+                    "\n# Set to true to disable the sign out link in the side menu. useful if you use auth.proxy, defaults to false" +
+                    "\n;disable_signout_menu = false" +
+                    "\n" +
+                    "\n#################################### Anonymous Auth ##########################";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = "#################################### Auth Proxy ##########################";
+                toReplace = "#################################### Generic OAuth ##########################" +
+                    "\n[auth.generic_oauth]" +
+                    "\n;enabled = false" +
+                    "\n;name = OAuth" +
+                    "\n;allow_sign_up = true" +
+                    "\n;client_id = some_id" +
+                    "\n;client_secret = some_secret" +
+                    "\n;scopes = user:email,read:org" +
+                    "\n;auth_url = https://foo.bar/login/oauth/authorize" +
+                    "\n;token_url = https://foo.bar/login/oauth/access_token" +
+                    "\n;api_url = https://foo.bar/user" +
+                    "\n;team_ids =" +
+                    "\n;allowed_organizations =" +
+                    "\n" +
+                    "\n#################################### Grafana.com Auth ####################" +
+                    "\n[auth.grafana_com]" +
+                    "\n;enabled = false" +
+                    "\n;allow_sign_up = true" +
+                    "\n;client_id = some_id" +
+                    "\n;client_secret = some_secret" +
+                    "\n;scopes = user:email" +
+                    "\n;allowed_organizations =" +
+                    "\n" +
+                    "\n#################################### Auth Proxy ##########################";
+                replace(content, toFind, toReplace);
+
+                toFind = "[emails]";
+                toReplace = ";from_name = Grafana" +
+                    "\n# EHLO identity in SMTP dialog (defaults to instance_name)" +
+                    "\n;ehlo_identity = dashboard.example.com" +
+                    "\n" +
+                    "\n[emails]";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = "# Either \"Trace\", \"Debug\", \"Info\", \"Warn\", \"Error\", \"Critical\", default is \"Trace\"";
+                toReplace = "# Either \"debug\", \"info\", \"warn\", \"error\", \"critical\", default is\"info\"";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = ";level = Info";
+                toReplace = ";level = info";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                toFind = "# Buffer length of channel, keep it as it is if you don't know what it is." +
+                    "\n;buffer_len = 10000";
+                toReplace = "# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug" +
+                    "\n;filters =";
+                replaceIfNotThere(content, toFind, toReplace);
+
+                Map<String, String> newProperties = new HashMap<>(1);
+                newProperties.put("content", content.toString());
+                updateConfigurationPropertiesForCluster(cluster, "ams-grafana-ini", newProperties, true, false);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
 }
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/utils/CustomStringUtils.java b/ambari-server/src/main/java/org/apache/ambari/server/utils/CustomStringUtils.java
index 330f4bf..16f00ae 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/utils/CustomStringUtils.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/utils/CustomStringUtils.java
@@ -67,4 +67,107 @@ public class CustomStringUtils {
       iterator.set(iterator.next().toUpperCase());
     }
   }
+
+  /**
+   * Insert a string after a substring
+   * @param toInsertInto the base string to be changed
+   * @param addAfter insert a string after this if found in <code>toInsertInto</code>
+   * @param toInsert insert this string
+   * @return if the <code>addAfter</code> argument occurs as a substring within this <code>toInsertInto</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   */
+  public static int insertAfter(StringBuilder toInsertInto, String addAfter, String toInsert) {
+    int index = toInsertInto.indexOf(addAfter);
+    if (index > -1) {
+      toInsertInto.insert(index + addAfter.length(), toInsert);
+    }
+    return index;
+  }
+
+  /**
+   * Insert a string after a substring if a specified string is not present already.
+   * @param toInsertInto the base string to be changed
+   * @param addAfter insert a string after this if found in <code>toInsertInto</code>
+   * @param toInsert insert this string
+   * @param ifNotThere only do the insert if this string is not found
+   * @return if the <code>addAfter</code> argument occurs as a substring within this <code>toInsertInto</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   * If the <code>ifNotThere</code> exists already in <code>toInsertInto</code>, -2 is returned.
+   */
+  public static int insertAfterIfNotThere(StringBuilder toInsertInto, String addAfter, String toInsert, String ifNotThere) {
+    if (toInsertInto.indexOf(ifNotThere) > -1) return -2;
+    return insertAfter(toInsertInto, addAfter, toInsert);
+  }
+
+  /**
+   * Insert a string after a substring if the string is not present already.
+   * @param toInsertInto the base string to be changed
+   * @param addAfter insert a string after this if found in <code>toInsertInto</code>
+   * @param toInsert insert this string
+   * @return if the <code>addAfter</code> argument occurs as a substring within this <code>toInsertInto</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   * If the <code>toInsert</code> exists already in <code>toInsertInto</code>, -2 is returned.
+   */
+  public static int insertAfterIfNotThere(StringBuilder toInsertInto, String addAfter, String toInsert) {
+    return insertAfterIfNotThere(toInsertInto, addAfter, toInsert, toInsert);
+  }
+
+  /**
+   * Delete a substring
+   * @param toDeleteFrom the base string to be changed
+   * @param toDelete delete this string from <code>toDeleteFrom</code> if found
+   * @return if the <code>toDelete</code> argument occurs as a substring within this <code>toDeleteFrom</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   */
+  public static  int deleteSubstring(StringBuilder toDeleteFrom, String toDelete) {
+    int index = toDeleteFrom.indexOf(toDelete);
+    if (index > -1) {
+      toDeleteFrom.delete(index, index + toDelete.length());
+    }
+    return index;
+  }
+
+  /**
+   * Replace a substring
+   * @param replaceIn the base string to be changed
+   * @param toFind replace this string with <code>toReplace</code> if found
+   * @param toReplace replace <code>toFind</code> string with this
+   * @return if the <code>toFind</code> argument occurs as a substring within this <code>replaceIn</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   */
+  public static int replace(StringBuilder replaceIn, String toFind, String toReplace) {
+    int index = replaceIn.indexOf(toFind);
+    if (index > -1) {
+      replaceIn.replace(index, index + toFind.length(), toReplace);
+    }
+    return index;
+  }
+
+  /**
+   * Replace a substring if a specified string is not present already.
+   * @param replaceIn the base string to be changed
+   * @param toFind replace this string with <code>toReplace</code> if found
+   * @param toReplace replace <code>toFind</code> string with this
+   * @param ifNotThere only do the replace if this string is not found
+   * @return if the <code>toFind</code> argument occurs as a substring within this <code>replaceIn</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   * If the <code>ifNotThere</code> exists already in <code>replaceIn</code>, -2 is returned.
+   */
+  public static int replaceIfNotThere(StringBuilder replaceIn, String toFind, String toReplace, String ifNotThere) {
+    if (replaceIn.indexOf(ifNotThere) > -1) return -2;
+    return replace(replaceIn, toFind, toReplace);
+  }
+
+  /**
+   * Replace a substring if a the string is not present already.
+   * @param replaceIn the base string to be changed
+   * @param toFind replace this string with <code>toReplace</code> if found
+   * @param toReplace replace <code>toFind</code> string with this
+   * @return if the <code>toFind</code> argument occurs as a substring within this <code>replaceIn</code>,
+   * then the index of the first character of the first such substring is returned; if it does not occur as a substring, -1 is returned.
+   * If the <code>toReplace</code> exists already in <code>replaceIn</code>, -2 is returned.
+   */
+  public static int replaceIfNotThere(StringBuilder replaceIn, String toFind, String toReplace) {
+    return replaceIfNotThere(replaceIn, toFind, toReplace, toReplace);
+  }
 }
diff --git a/ambari-server/src/main/resources/common-services/AMBARI_METRICS/0.2.0/configuration/ams-grafana-ini.xml b/ambari-server/src/main/resources/common-services/AMBARI_METRICS/0.2.0/configuration/ams-grafana-ini.xml
index cdc7efa..9c4c499 100644
--- a/ambari-server/src/main/resources/common-services/AMBARI_METRICS/0.2.0/configuration/ams-grafana-ini.xml
+++ b/ambari-server/src/main/resources/common-services/AMBARI_METRICS/0.2.0/configuration/ams-grafana-ini.xml
@@ -130,7 +130,7 @@ cert_key = {{ams_grafana_cert_key}}
 ;host = 127.0.0.1:3306
 ;name = grafana
 ;user = root
-# If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
 ;password =
 
 # Use either URL or the previous fields to configure the database
@@ -180,7 +180,7 @@ cert_key = {{ams_grafana_cert_key}}
 ;reporting_enabled = true
 
 # Set to false to disable all checks to https://grafana.net
-# for new vesions (grafana itself and plugins), check is used
+# for new versions (grafana itself and plugins), check is used
 # in some UI views to notify that grafana or plugin update exists
 # This option does not cause any auto updates, nor send any information
 # only a GET request to http://grafana.com to get latest versions
@@ -252,7 +252,7 @@ admin_user = {{ams_grafana_admin_user}}
 # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
 ;disable_login_form = false
 
-# Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false
+# Set to true to disable the sign out link in the side menu. useful if you use auth.proxy, defaults to false
 ;disable_signout_menu = false
 
 #################################### Anonymous Auth ##########################
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/utils/CustomStringUtilsTest.java b/ambari-server/src/test/java/org/apache/ambari/server/utils/CustomStringUtilsTest.java
new file mode 100644
index 0000000..bc05159
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/utils/CustomStringUtilsTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.utils;
+
+import static junit.framework.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class CustomStringUtilsTest {
+  @Test
+  public void testInsertAfter() {
+    final String baseText = "abcdefghijklmnopqr";
+    StringBuilder content = new StringBuilder(baseText);
+    int res = CustomStringUtils.insertAfter(content, "abcdefghijklmnopqr", "xxx");
+    assertEquals(0, res);
+    assertEquals("abcdefghijklmnopqrxxx", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.insertAfter(content, "a", "x");
+    assertEquals(0, res);
+    assertEquals("axbcdefghijklmnopqr", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.insertAfter(content, "x", "y");
+    assertEquals(-1, res);
+    assertEquals(baseText, content.toString());
+  }
+
+  @Test
+  public void testDeleteSubstring() {
+    final String baseText = "abcdefghijklmnopqr";
+    StringBuilder content = new StringBuilder(baseText);
+    int res = CustomStringUtils.deleteSubstring(content, "a");
+    assertEquals(0, res);
+    assertEquals("bcdefghijklmnopqr", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.deleteSubstring(content, "r");
+    assertEquals(17, res);
+    assertEquals("abcdefghijklmnopq", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.deleteSubstring(content, "efgh");
+    assertEquals(4, res);
+    assertEquals("abcdijklmnopqr", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.deleteSubstring(content, baseText);
+    assertEquals(0, res);
+    assertEquals("", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.deleteSubstring(content, "x");
+    assertEquals(-1, res);
+    assertEquals(baseText, content.toString());
+  }
+
+  @Test
+  public void testReplace() {
+    final String baseText = "abcdefghijklmnopqr";
+    StringBuilder content = new StringBuilder(baseText);
+    int res = CustomStringUtils.replace(content, "abcdefghijklmnopqr", "xxx");
+    assertEquals(0, res);
+    assertEquals("xxx", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.replace(content, "abcdefghijklmnopqr", "xxx");
+    assertEquals(0, res);
+    assertEquals("xxx", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.replace(content, "fghijk", "xxx");
+    assertEquals(5, res);
+    assertEquals("abcdexxxlmnopqr", content.toString());
+
+    content = new StringBuilder(baseText);
+    res = CustomStringUtils.replace(content, "xxxx", "yyyy");
+    assertEquals(-1, res);
+    assertEquals(baseText, content.toString());
+  }
+}
\ No newline at end of file