*/
package org.sonar.application.config;
+import com.google.common.net.InetAddresses;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
+import org.apache.commons.lang.StringUtils;
import org.sonar.process.MessageException;
import org.sonar.process.ProcessId;
import org.sonar.process.ProcessProperties;
import org.sonar.process.Props;
+import static com.google.common.net.InetAddresses.forString;
import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static org.apache.commons.lang.StringUtils.isBlank;
import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
import static org.sonar.process.ProcessProperties.CLUSTER_WEB_LEADER;
import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
public class ClusterSettings implements Consumer<Props> {
if (!isClusterEnabled(props)) {
return;
}
+
+ checkProperties(props);
+ }
+
+ private static void checkProperties(Props props) {
+ // Cluster web leader is not allowed
if (props.value(CLUSTER_WEB_LEADER) != null) {
throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_LEADER));
}
+
+ // Mandatory properties
+ ensureMandatoryProperty(props, SEARCH_HOST);
+ ensureMandatoryProperty(props, CLUSTER_HOSTS);
+ ensureMandatoryProperty(props, CLUSTER_SEARCH_HOSTS);
+
+ // H2 Database is not allowed in cluster mode
String jdbcUrl = props.value(JDBC_URL);
- if (jdbcUrl == null || jdbcUrl.startsWith("jdbc:h2:")) {
+ if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
throw new MessageException("Embedded database is not supported in cluster mode");
}
+
+ // Loopback interfaces are forbidden for SEARCH_HOST and CLUSTER_NETWORK_INTERFACES
+ ensureNotLoopback(props, SEARCH_HOST);
+ ensureNotLoopback(props, CLUSTER_HOSTS);
+ ensureNotLoopback(props, CLUSTER_NETWORK_INTERFACES);
+ ensureNotLoopback(props, CLUSTER_SEARCH_HOSTS);
+
+ ensureLocalAddress(props, SEARCH_HOST);
+ ensureLocalAddress(props, CLUSTER_NETWORK_INTERFACES);
+ }
+
+ private static void ensureMandatoryProperty(Props props, String key) {
+ if (isBlank(props.value(key))) {
+ throw new MessageException(format("Property [%s] is mandatory", key));
+ }
+ }
+
+ private static void ensureNotLoopback(Props props, String key) {
+ String ipList = props.value(key);
+ if (ipList == null) {
+ return;
+ }
+
+ stream(ipList.split(","))
+ .filter(StringUtils::isNotBlank)
+ .map(StringUtils::trim)
+ .forEach(ip -> {
+ InetAddress inetAddress = convertToInetAddress(ip, key);
+ if (inetAddress.isLoopbackAddress()) {
+ throw new MessageException(format("The interface address [%s] of [%s] must not be a loopback address", ip, key));
+ }
+ });
+ }
+
+ private static void ensureLocalAddress(Props props, String key) {
+ String ipList = props.value(key);
+
+ if (ipList == null) {
+ return;
+ }
+
+ stream(ipList.split(","))
+ .filter(StringUtils::isNotBlank)
+ .map(StringUtils::trim)
+ .forEach(ip -> {
+ InetAddress inetAddress = convertToInetAddress(ip, key);
+ try {
+ if (NetworkInterface.getByInetAddress(inetAddress) == null) {
+ throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+ }
+ } catch (SocketException e) {
+ throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+ }
+ });
+ }
+
+ private static InetAddress convertToInetAddress(String text, String key) {
+ InetAddress inetAddress;
+
+ if (!InetAddresses.isInetAddress(text)) {
+ try {
+ inetAddress =InetAddress.getByName(text);
+ } catch (UnknownHostException e) {
+ throw new MessageException(format("The interface address [%s] of [%s] cannot be resolved : %s", text, key, e.getMessage()));
+ }
+ } else {
+ inetAddress = forString(text);
+ }
+
+ return inetAddress;
}
public static boolean isClusterEnabled(AppSettings settings) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Enumeration;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.process.MessageException;
+
+import static java.lang.String.format;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
+@RunWith(Theories.class)
+public class ClusterSettingsLoopbackTest {
+
+ private TestAppSettings settings;
+
+ @DataPoints("loopback_with_single_ip")
+ public static final String[] LOOPBACK_SINGLE_IP = {
+ "localhost",
+ "ip6-localhost",
+ "127.0.0.1",
+ "127.1.1.1",
+ "127.243.136.241",
+ "::1",
+ "0:0:0:0:0:0:0:1"
+ };
+
+ @DataPoints("loopback_with_multiple_ips")
+ public static final String[] LOOPBACK_IPS = {
+ "localhost",
+ "ip6-localhost",
+ "127.0.0.1",
+ "127.1.1.1",
+ "127.243.136.241",
+ "::1",
+ "0:0:0:0:0:0:0:1",
+ "127.0.0.1,192.168.11.25",
+ "192.168.11.25,127.1.1.1",
+ "2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,0:0:0:0:0:0:0:1",
+ "0:0:0:0:0:0:0:1,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb",
+ "2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,::1",
+ "::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb",
+ "::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb"
+ };
+
+ @DataPoints("key_for_single_ip")
+ public static final String[] PROPERTY_KEYS_WITH_SINGLE_IP = {
+ SEARCH_HOST
+ };
+
+ @DataPoints("key_for_multiple_ips")
+ public static final String[] PROPERTY_KEYS_WITH_MULTIPLE_IPS = {
+ CLUSTER_NETWORK_INTERFACES,
+ CLUSTER_SEARCH_HOSTS,
+ CLUSTER_HOSTS
+ };
+
+ @DataPoints("key_with_local_ip")
+ public static final String[] PROPERTY_KEYS_ALL = {
+ CLUSTER_NETWORK_INTERFACES,
+ SEARCH_HOST
+ };
+
+ @DataPoints("not_local_address")
+ public static final String[] NOT_LOCAL_ADDRESS = {
+ "www.sonarqube.org",
+ "www.google.fr",
+ "www.google.com, www.sonarsource.com, wwww.sonarqube.org"
+ };
+
+ @DataPoints("unresolvable_hosts")
+ public static final String[] UNRESOLVABLE_HOSTS = {
+ "...",
+ "භඦආ\uD801\uDC8C\uD801\uDC8B"
+ };
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Before
+ public void resetSettings() {
+ settings = getClusterSettings();
+ }
+
+ @Theory
+ public void accept_throws_MessageException_if_not_local_address(
+ @FromDataPoints("key_with_local_ip") String propertyKey,
+ @FromDataPoints("not_local_address") String inet) {
+ settings.set(propertyKey, inet);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(" is not a local address");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Theory
+ public void accept_throws_MessageException_if_unresolvable_host(
+ @FromDataPoints("key_with_local_ip") String propertyKey,
+ @FromDataPoints("unresolvable_hosts") String inet) {
+ settings.set(propertyKey, inet);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(" cannot be resolved");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Theory
+ public void accept_throws_MessageException_if_loopback(
+ @FromDataPoints("key_for_single_ip") String propertyKey,
+ @FromDataPoints("loopback_with_single_ip") String inet) {
+ settings.set(propertyKey, inet);
+ checkLoopback(propertyKey);
+ }
+
+ @Theory
+ public void accept_throws_MessageException_if_loopback_for_multiple_ips(
+ @FromDataPoints("key_for_multiple_ips") String propertyKey,
+ @FromDataPoints("loopback_with_multiple_ips") String inet) {
+ settings.set(propertyKey, inet);
+ checkLoopback(propertyKey);
+ }
+
+ private void checkLoopback(String key) {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(format(" of [%s] must not be a loopback address", key));
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ private static TestAppSettings getClusterSettings() {
+ String localAddress = null;
+ try {
+ Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+ for (NetworkInterface networkInterface : Collections.list(nets)) {
+ if (!networkInterface.isLoopback() && networkInterface.isUp()) {
+ localAddress = networkInterface.getInetAddresses().nextElement().getHostAddress();
+ }
+ }
+ if (localAddress == null) {
+ throw new RuntimeException("Cannot find a non loopback card required for tests");
+ }
+
+ } catch (SocketException e) {
+ throw new RuntimeException("Cannot find a non loopback card required for tests");
+ }
+
+ TestAppSettings testAppSettings = new TestAppSettings()
+ .set(CLUSTER_ENABLED, "true")
+ .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(SEARCH_HOST, localAddress)
+ .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+ return testAppSettings;
+ }
+}
*/
package org.sonar.application.config;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.process.MessageException;
import org.sonar.process.ProcessProperties;
+import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
import static org.sonar.process.ProcessId.ELASTICSEARCH;
import static org.sonar.process.ProcessId.WEB_SERVER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
public class ClusterSettingsTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
- private TestAppSettings settings = new TestAppSettings();
+ private TestAppSettings settings;
+
+ @Before
+ public void resetSettings() {
+ settings = getClusterSettings();
+ }
@Test
public void test_isClusterEnabled() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
- settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+ settings.set(CLUSTER_ENABLED, "false");
assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
}
@Test
public void isClusterEnabled_returns_false_by_default() {
- assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
+ assertThat(ClusterSettings.isClusterEnabled(new TestAppSettings())).isFalse();
}
@Test
@Test
public void getEnabledProcesses_returns_all_processes_by_default_in_cluster_mode() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
}
@Test
public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
@Test
public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
settings.set("sonar.cluster.web.startupLeader", "true");
expectedException.expect(MessageException.class);
@Test
public void accept_does_nothing_if_cluster_is_disabled() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+ settings.set(CLUSTER_ENABLED, "false");
// this property is supposed to fail if cluster is enabled
settings.set("sonar.cluster.web.startupLeader", "true");
@Test
public void accept_throws_MessageException_if_h2() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
settings.set("sonar.jdbc.url", "jdbc:h2:mem");
expectedException.expect(MessageException.class);
@Test
public void accept_throws_MessageException_if_default_jdbc_url() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.clearProperty(JDBC_URL);
expectedException.expect(MessageException.class);
expectedException.expectMessage("Embedded database is not supported in cluster mode");
@Test
public void isLocalElasticsearchEnabled_returns_true_by_default_in_cluster_mode() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
-
assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
}
@Test
public void isLocalElasticsearchEnabled_returns_false_if_local_es_node_is_disabled_in_cluster_mode() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_ENABLED, "true");
settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
}
+
+ @Test
+ public void accept_throws_MessageException_if_searchHost_is_missing() {
+ settings.clearProperty(SEARCH_HOST);
+ checkMandatoryProperty(SEARCH_HOST);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_searchHost_is_blank() {
+ settings.set(SEARCH_HOST, " ");
+ checkMandatoryProperty(SEARCH_HOST);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterHosts_is_missing() {
+ settings.clearProperty(CLUSTER_HOSTS);
+ checkMandatoryProperty(CLUSTER_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterHosts_is_blank() {
+ settings.set(CLUSTER_HOSTS, " ");
+ checkMandatoryProperty(CLUSTER_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() {
+ settings.clearProperty(CLUSTER_SEARCH_HOSTS);
+ checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterSearchHosts_is_blank() {
+ settings.set(CLUSTER_SEARCH_HOSTS, " ");
+ checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+ }
+
+ private void checkMandatoryProperty(String key) {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(format("Property [%s] is mandatory", key));
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ private static TestAppSettings getClusterSettings() {
+ TestAppSettings testAppSettings = new TestAppSettings()
+ .set(CLUSTER_ENABLED, "true")
+ .set(CLUSTER_SEARCH_HOSTS, "localhost")
+ .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(SEARCH_HOST, "192.168.233.1")
+ .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+ return testAppSettings;
+ }
}
*/
public class TestAppSettings implements AppSettings {
- private Props properties;
+ private Props props;
public TestAppSettings() {
- this.properties = new Props(new Properties());
- ProcessProperties.completeDefaults(this.properties);
+ this.props = new Props(new Properties());
+ ProcessProperties.completeDefaults(this.props);
}
public TestAppSettings set(String key, String value) {
- this.properties.set(key, value);
+ this.props.set(key, value);
return this;
}
@Override
public Props getProps() {
- return properties;
+ return props;
}
@Override
public Optional<String> getValue(String key) {
- return Optional.ofNullable(properties.value(key));
+ return Optional.ofNullable(props.value(key));
}
@Override
public void reload(Props copy) {
- this.properties = copy;
+ this.props = copy;
+ }
+
+ public void clearProperty(String key) {
+ this.props.rawProperties().remove(key);
}
}
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
-import org.sonar.application.process.JavaCommand;
-import org.sonar.application.process.JavaProcessLauncher;
-import org.sonar.application.process.JavaProcessLauncherImpl;
-import org.sonar.application.process.ProcessMonitor;
import org.sonar.process.AllProcessesCommands;
import org.sonar.process.ProcessId;