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