@@ -24,6 +24,8 @@ import org.sonar.ce.monitoring.CeTasksMBeanImpl; | |||
import org.sonar.ce.queue.CeQueueInitializer; | |||
import org.sonar.ce.queue.InternalCeQueueImpl; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.process.systeminfo.JvmPropertiesSection; | |||
import org.sonar.process.systeminfo.JvmStateSection; | |||
public class CeQueueModule extends Module { | |||
@Override | |||
@@ -35,7 +37,9 @@ public class CeQueueModule extends Module { | |||
// queue monitoring | |||
CEQueueStatusImpl.class, | |||
CeTasksMBeanImpl.class, | |||
new JvmStateSection("Compute Engine JVM State"), | |||
new JvmPropertiesSection("Compute Engine JVM Properties"), | |||
// init queue state and queue processing | |||
CeQueueInitializer.class); | |||
} |
@@ -49,7 +49,6 @@ import org.sonar.ce.StandaloneCeDistributedInformation; | |||
import org.sonar.ce.cleaning.CeCleaningModule; | |||
import org.sonar.ce.db.ReadOnlyPropertiesDao; | |||
import org.sonar.ce.log.CeProcessLogging; | |||
import org.sonar.ce.monitoring.CeSystemInfoModule; | |||
import org.sonar.ce.platform.ComputeEngineExtensionInstaller; | |||
import org.sonar.ce.queue.CeQueueCleaner; | |||
import org.sonar.ce.queue.PurgeCeActivities; | |||
@@ -127,6 +126,9 @@ import org.sonar.server.platform.UrlSettings; | |||
import org.sonar.server.platform.WebServerImpl; | |||
import org.sonar.server.platform.db.migration.MigrationConfigurationModule; | |||
import org.sonar.server.platform.db.migration.version.DatabaseVersion; | |||
import org.sonar.server.platform.monitoring.DatabaseSection; | |||
import org.sonar.server.platform.monitoring.cluster.ProcessInfoProvider; | |||
import org.sonar.server.platform.monitoring.cluster.LoggingSection; | |||
import org.sonar.server.plugins.InstalledPluginReferentialFactory; | |||
import org.sonar.server.plugins.ServerExtensionInstaller; | |||
import org.sonar.server.plugins.privileged.PrivilegedPluginsBootstraper; | |||
@@ -423,8 +425,14 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { | |||
if (props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED)) { | |||
container.add( | |||
StartableHazelcastMember.class, | |||
CeDistributedInformationImpl.class); | |||
container.add(CeSystemInfoModule.forClusterMode()); | |||
// system health | |||
CeDistributedInformationImpl.class, | |||
// system info | |||
DatabaseSection.class, | |||
ProcessInfoProvider.class, | |||
LoggingSection.class); | |||
} else { | |||
container.add(StandaloneCeDistributedInformation.class); | |||
} |
@@ -131,7 +131,7 @@ public class ComputeEngineContainerImplTest { | |||
CONTAINER_ITSELF | |||
+ 73 // level 4 | |||
+ 4 // content of CeConfigurationModule | |||
+ 4 // content of CeQueueModule | |||
+ 6 // content of CeQueueModule | |||
+ 4 // content of CeHttpModule | |||
+ 3 // content of CeTaskCommonsModule | |||
+ 4 // content of ProjectAnalysisTaskModule |
@@ -12,6 +12,8 @@ | |||
<artifactId>sonar-main</artifactId> | |||
<name>SonarQube :: Main Process</name> | |||
<description>Server process used to bootstrap Elasticsearch, Web Server and | |||
Compute Engine processes. Could be merged with sonar-application.</description> | |||
<properties> | |||
<!-- |
@@ -10,6 +10,7 @@ | |||
<artifactId>sonar-process</artifactId> | |||
<name>SonarQube :: Process</name> | |||
<description>Library shared by all kinds of server processes: main, web and compute engine</description> | |||
<properties> | |||
<sonar.exclusions>target/generated-sources/**/*</sonar.exclusions> |
@@ -21,7 +21,9 @@ package org.sonar.process.cluster.hz; | |||
import com.hazelcast.core.Member; | |||
import java.io.IOException; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
@@ -30,9 +32,21 @@ import static org.mockito.Mockito.when; | |||
public class DistributedAnswerTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private Member member = newMember("member1"); | |||
private DistributedAnswer underTest = new DistributedAnswer(); | |||
@Test | |||
public void getMembers_return_all_members() { | |||
underTest.setAnswer(member, "foo"); | |||
underTest.setTimedOut(newMember("bar")); | |||
underTest.setFailed(newMember("baz"), new IOException("BOOM")); | |||
assertThat(underTest.getMembers()).hasSize(3); | |||
} | |||
@Test | |||
public void test_call_with_unknown_member() { | |||
assertThat(underTest.getAnswer(member)).isEmpty(); | |||
@@ -46,6 +60,7 @@ public class DistributedAnswerTest { | |||
assertThat(underTest.getAnswer(member)).hasValue("foo"); | |||
assertThat(underTest.hasTimedOut(member)).isFalse(); | |||
assertThat(underTest.getFailed(member)).isEmpty(); | |||
} | |||
@Test | |||
@@ -54,6 +69,7 @@ public class DistributedAnswerTest { | |||
assertThat(underTest.getAnswer(member)).isEmpty(); | |||
assertThat(underTest.hasTimedOut(member)).isTrue(); | |||
assertThat(underTest.getFailed(member)).isEmpty(); | |||
} | |||
@Test | |||
@@ -61,6 +77,8 @@ public class DistributedAnswerTest { | |||
IOException e = new IOException(); | |||
underTest.setFailed(member, e); | |||
assertThat(underTest.getAnswer(member)).isEmpty(); | |||
assertThat(underTest.hasTimedOut(member)).isFalse(); | |||
assertThat(underTest.getFailed(member)).hasValue(e); | |||
} | |||
@@ -76,9 +94,46 @@ public class DistributedAnswerTest { | |||
assertThat(underTest.getFailed(member)).hasValue(exception); | |||
} | |||
@Test | |||
public void propagateExceptions_does_nothing_if_no_members() { | |||
// no errors | |||
underTest.propagateExceptions(); | |||
} | |||
@Test | |||
public void propagateExceptions_does_nothing_if_no_errors() { | |||
underTest.setAnswer(newMember("foo"), "bar"); | |||
// no errors | |||
underTest.propagateExceptions(); | |||
} | |||
@Test | |||
public void propagateExceptions_throws_ISE_if_at_least_one_timeout() { | |||
underTest.setAnswer(newMember("bar"), "baz"); | |||
underTest.setTimedOut(newMember("foo")); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Distributed cluster action timed out in cluster nodes foo"); | |||
underTest.propagateExceptions(); | |||
} | |||
@Test | |||
public void propagateExceptions_throws_ISE_if_at_least_one_failure() { | |||
underTest.setAnswer(newMember("bar"), "baz"); | |||
underTest.setFailed(newMember("foo"), new IOException("BOOM")); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Distributed cluster action in cluster nodes foo (other nodes may have timed out)"); | |||
underTest.propagateExceptions(); | |||
} | |||
private static Member newMember(String uuid) { | |||
Member member = mock(Member.class); | |||
when(member.getUuid()).thenReturn(uuid); | |||
when(member.getStringAttribute(HazelcastMember.Attribute.NODE_NAME)).thenReturn(uuid); | |||
return member; | |||
} | |||
} |
@@ -22,7 +22,6 @@ package org.sonar.server.cluster; | |||
import com.hazelcast.core.Cluster; | |||
import com.hazelcast.core.IAtomicReference; | |||
import com.hazelcast.core.MemberSelector; | |||
import java.net.InetAddress; | |||
import java.net.UnknownHostException; | |||
import java.util.List; | |||
import java.util.Map; | |||
@@ -55,12 +54,12 @@ import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE; | |||
public class StartableHazelcastMember implements HazelcastMember, Startable { | |||
private final Configuration config; | |||
private final NetworkUtils networkUtils; | |||
private final NetworkUtils network; | |||
private HazelcastMember member = null; | |||
public StartableHazelcastMember(Configuration config, NetworkUtils networkUtils) { | |||
public StartableHazelcastMember(Configuration config, NetworkUtils network) { | |||
this.config = config; | |||
this.networkUtils = networkUtils; | |||
this.network = network; | |||
} | |||
@Override | |||
@@ -136,7 +135,7 @@ public class StartableHazelcastMember implements HazelcastMember, Startable { | |||
String networkAddress = config.get(CLUSTER_NODE_HOST).orElseThrow(() -> new IllegalStateException("Missing node host")); | |||
int freePort; | |||
try { | |||
freePort = networkUtils.getNextAvailablePort(InetAddress.getByName(networkAddress)); | |||
freePort = network.getNextAvailablePort(network.toInetAddress(networkAddress)); | |||
} catch (UnknownHostException e) { | |||
throw new IllegalStateException(format("Can not resolve address %s", networkAddress), e); | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.health; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import org.sonar.process.cluster.health.NodeHealth; | |||
@@ -43,6 +44,12 @@ public class ClusterHealth { | |||
return nodes; | |||
} | |||
public Optional<NodeHealth> getNodeHealth(String nodeName) { | |||
return nodes.stream() | |||
.filter(node -> nodeName.equals(node.getDetails().getName())) | |||
.findFirst(); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { |
@@ -60,29 +60,39 @@ public class EsStateSection implements SystemInfoSection { | |||
} | |||
private void completeNodeAttributes(ProtobufSystemInfo.Section.Builder protobuf) { | |||
NodesStatsResponse nodesStats = esClient.prepareNodesStats().all().get(); | |||
NodesStatsResponse nodesStats = esClient.prepareNodesStats() | |||
.setFs(true) | |||
.setProcess(true) | |||
.setJvm(true) | |||
.setIndices(true) | |||
.setBreaker(true) | |||
.get(); | |||
if (!nodesStats.getNodes().isEmpty()) { | |||
NodeStats stats = nodesStats.getNodes().get(0); | |||
setAttribute(protobuf, "Disk Available", byteCountToDisplaySize(stats.getFs().getTotal().getAvailable().getBytes())); | |||
setAttribute(protobuf, "Store Size", byteCountToDisplaySize(stats.getIndices().getStore().getSizeInBytes())); | |||
setAttribute(protobuf, "Open File Descriptors", stats.getProcess().getOpenFileDescriptors()); | |||
setAttribute(protobuf, "Max File Descriptors", stats.getProcess().getMaxFileDescriptors()); | |||
setAttribute(protobuf, "Spinning", stats.getFs().getTotal().getSpins()); | |||
setAttribute(protobuf, "JVM Heap Usage", formatPercent(stats.getJvm().getMem().getHeapUsedPercent())); | |||
setAttribute(protobuf, "JVM Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getHeapUsed().getBytes())); | |||
setAttribute(protobuf, "JVM Heap Max", byteCountToDisplaySize(stats.getJvm().getMem().getHeapMax().getBytes())); | |||
setAttribute(protobuf, "JVM Non Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getNonHeapUsed().getBytes())); | |||
setAttribute(protobuf, "JVM Threads", stats.getJvm().getThreads().getCount()); | |||
setAttribute(protobuf, "Field Data Memory", byteCountToDisplaySize(stats.getIndices().getFieldData().getMemorySizeInBytes())); | |||
setAttribute(protobuf, "Field Data Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getLimit())); | |||
setAttribute(protobuf, "Field Data Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getEstimated())); | |||
setAttribute(protobuf, "Request Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getLimit())); | |||
setAttribute(protobuf, "Request Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getEstimated())); | |||
setAttribute(protobuf, "Query Cache Memory", byteCountToDisplaySize(stats.getIndices().getQueryCache().getMemorySizeInBytes())); | |||
setAttribute(protobuf, "Request Cache Memory", byteCountToDisplaySize(stats.getIndices().getRequestCache().getMemorySizeInBytes())); | |||
toProtobuf(stats, protobuf); | |||
} | |||
} | |||
public static void toProtobuf(NodeStats stats, ProtobufSystemInfo.Section.Builder protobuf) { | |||
setAttribute(protobuf, "Disk Available", byteCountToDisplaySize(stats.getFs().getTotal().getAvailable().getBytes())); | |||
setAttribute(protobuf, "Store Size", byteCountToDisplaySize(stats.getIndices().getStore().getSizeInBytes())); | |||
setAttribute(protobuf, "Open File Descriptors", stats.getProcess().getOpenFileDescriptors()); | |||
setAttribute(protobuf, "Max File Descriptors", stats.getProcess().getMaxFileDescriptors()); | |||
setAttribute(protobuf, "Spinning", stats.getFs().getTotal().getSpins()); | |||
setAttribute(protobuf, "JVM Heap Usage", formatPercent(stats.getJvm().getMem().getHeapUsedPercent())); | |||
setAttribute(protobuf, "JVM Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getHeapUsed().getBytes())); | |||
setAttribute(protobuf, "JVM Heap Max", byteCountToDisplaySize(stats.getJvm().getMem().getHeapMax().getBytes())); | |||
setAttribute(protobuf, "JVM Non Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getNonHeapUsed().getBytes())); | |||
setAttribute(protobuf, "JVM Threads", stats.getJvm().getThreads().getCount()); | |||
setAttribute(protobuf, "Field Data Memory", byteCountToDisplaySize(stats.getIndices().getFieldData().getMemorySizeInBytes())); | |||
setAttribute(protobuf, "Field Data Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getLimit())); | |||
setAttribute(protobuf, "Field Data Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getEstimated())); | |||
setAttribute(protobuf, "Request Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getLimit())); | |||
setAttribute(protobuf, "Request Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getEstimated())); | |||
setAttribute(protobuf, "Query Cache Memory", byteCountToDisplaySize(stats.getIndices().getQueryCache().getMemorySizeInBytes())); | |||
setAttribute(protobuf, "Request Cache Memory", byteCountToDisplaySize(stats.getIndices().getRequestCache().getMemorySizeInBytes())); | |||
} | |||
private ClusterStatsResponse clusterStats() { | |||
return esClient.prepareClusterStats().get(); | |||
} |
@@ -32,8 +32,6 @@ import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.authentication.IdentityProviderRepository; | |||
import org.sonar.server.health.Health; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.platform.ServerIdLoader; | |||
import org.sonar.server.platform.ServerLogging; | |||
import org.sonar.server.user.SecurityRealmFactory; | |||
@@ -51,11 +49,10 @@ public class StandaloneSystemSection extends BaseSectionMBean implements SystemS | |||
private final ServerLogging serverLogging; | |||
private final ServerIdLoader serverIdLoader; | |||
private final OfficialDistribution officialDistribution; | |||
private final HealthChecker healthChecker; | |||
public StandaloneSystemSection(Configuration config, SecurityRealmFactory securityRealmFactory, | |||
IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging, | |||
ServerIdLoader serverIdLoader, OfficialDistribution officialDistribution, HealthChecker healthChecker) { | |||
ServerIdLoader serverIdLoader, OfficialDistribution officialDistribution) { | |||
this.config = config; | |||
this.securityRealmFactory = securityRealmFactory; | |||
this.identityProviderRepository = identityProviderRepository; | |||
@@ -63,7 +60,6 @@ public class StandaloneSystemSection extends BaseSectionMBean implements SystemS | |||
this.serverLogging = serverLogging; | |||
this.serverIdLoader = serverIdLoader; | |||
this.officialDistribution = officialDistribution; | |||
this.healthChecker = healthChecker; | |||
} | |||
@Override | |||
@@ -123,9 +119,6 @@ public class StandaloneSystemSection extends BaseSectionMBean implements SystemS | |||
setAttribute(protobuf, "Server ID", serverId.getId()); | |||
setAttribute(protobuf, "Server ID validated", serverId.isValid()); | |||
}); | |||
Health health = healthChecker.checkNode(); | |||
setAttribute(protobuf, "Health", health.getStatus().name()); | |||
setAttribute(protobuf, "Health Causes", health.getCauses()); | |||
setAttribute(protobuf, "Version", getVersion()); | |||
setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication()); | |||
addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders()); |
@@ -25,9 +25,11 @@ import org.sonar.server.platform.monitoring.cluster.AppNodesInfoLoaderImpl; | |||
import org.sonar.server.platform.monitoring.cluster.GlobalInfoLoader; | |||
import org.sonar.server.platform.monitoring.cluster.GlobalSystemSection; | |||
import org.sonar.server.platform.monitoring.cluster.ProcessInfoProvider; | |||
import org.sonar.server.platform.monitoring.cluster.LoggingSection; | |||
import org.sonar.server.platform.monitoring.cluster.NodeSystemSection; | |||
import org.sonar.server.platform.monitoring.cluster.SearchNodesInfoLoaderImpl; | |||
import org.sonar.server.platform.ws.ClusterInfoAction; | |||
import org.sonar.server.platform.ws.StandaloneInfoAction; | |||
import org.sonar.server.platform.ws.InfoAction; | |||
public class WebSystemInfoModule { | |||
@@ -48,7 +50,7 @@ public class WebSystemInfoModule { | |||
OfficialDistribution.class, | |||
StandaloneInfoAction.class | |||
InfoAction.class | |||
}; | |||
} | |||
@@ -59,6 +61,7 @@ public class WebSystemInfoModule { | |||
DatabaseSection.class, | |||
EsStatisticsSection.class, | |||
GlobalSystemSection.class, | |||
LoggingSection.class, | |||
NodeSystemSection.class, | |||
PluginsSection.class, | |||
SettingsSection.class, | |||
@@ -68,6 +71,7 @@ public class WebSystemInfoModule { | |||
ProcessInfoProvider.class, | |||
GlobalInfoLoader.class, | |||
AppNodesInfoLoaderImpl.class, | |||
SearchNodesInfoLoaderImpl.class, | |||
ClusterInfoAction.class | |||
}; | |||
} |
@@ -50,9 +50,6 @@ public class AppNodesInfoLoaderImpl implements AppNodesInfoLoader { | |||
if (nodeInfo == null) { | |||
nodeInfo = new NodeInfo(nodeName); | |||
nodesByName.put(nodeName, nodeInfo); | |||
String hostname = member.getStringAttribute(HazelcastMember.Attribute.HOSTNAME); | |||
nodeInfo.setAttribute("Hostname", hostname); | |||
} | |||
completeNodeInfo(distributedAnswer, member, nodeInfo); | |||
} | |||
@@ -64,13 +61,13 @@ public class AppNodesInfoLoaderImpl implements AppNodesInfoLoader { | |||
} | |||
} | |||
private void completeNodeInfo(DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer, Member member, NodeInfo nodeInfo) { | |||
private static void completeNodeInfo(DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer, Member member, NodeInfo nodeInfo) { | |||
Optional<ProtobufSystemInfo.SystemInfo> nodeAnswer = distributedAnswer.getAnswer(member); | |||
Optional<Exception> failure = distributedAnswer.getFailed(member); | |||
if (distributedAnswer.hasTimedOut(member)) { | |||
nodeInfo.setAttribute("Error", "Failed to retrieve information on time"); | |||
nodeInfo.setErrorMessage("Failed to retrieve information on time"); | |||
} else if (failure.isPresent()) { | |||
nodeInfo.setAttribute("Error", "Failed to retrieve information: " + failure.get().getMessage()); | |||
nodeInfo.setErrorMessage("Failed to retrieve information: " + failure.get().getMessage()); | |||
} else if (nodeAnswer.isPresent()) { | |||
nodeAnswer.get().getSectionsList().forEach(nodeInfo::addSection); | |||
} |
@@ -33,8 +33,6 @@ import org.sonar.process.systeminfo.Global; | |||
import org.sonar.process.systeminfo.SystemInfoSection; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.authentication.IdentityProviderRepository; | |||
import org.sonar.server.health.ClusterHealth; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.platform.ServerIdLoader; | |||
import org.sonar.server.user.SecurityRealmFactory; | |||
@@ -48,15 +46,13 @@ public class GlobalSystemSection implements SystemInfoSection, Global { | |||
private final ServerIdLoader serverIdLoader; | |||
private final SecurityRealmFactory securityRealmFactory; | |||
private final IdentityProviderRepository identityProviderRepository; | |||
private final HealthChecker healthChecker; | |||
public GlobalSystemSection(Configuration config, ServerIdLoader serverIdLoader, SecurityRealmFactory securityRealmFactory, | |||
IdentityProviderRepository identityProviderRepository, HealthChecker healthChecker) { | |||
IdentityProviderRepository identityProviderRepository) { | |||
this.config = config; | |||
this.serverIdLoader = serverIdLoader; | |||
this.securityRealmFactory = securityRealmFactory; | |||
this.identityProviderRepository = identityProviderRepository; | |||
this.healthChecker = healthChecker; | |||
} | |||
@Override | |||
@@ -64,10 +60,6 @@ public class GlobalSystemSection implements SystemInfoSection, Global { | |||
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); | |||
protobuf.setName("System"); | |||
ClusterHealth health = healthChecker.checkCluster(); | |||
setAttribute(protobuf, "Health", health.getHealth().getStatus().name()); | |||
setAttribute(protobuf, "Health Causes", health.getHealth().getCauses()); | |||
serverIdLoader.get().ifPresent(serverId -> { | |||
setAttribute(protobuf, "Server ID", serverId.getId()); | |||
setAttribute(protobuf, "Server ID validated", serverId.isValid()); |
@@ -0,0 +1,55 @@ | |||
/* | |||
* 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.server.platform.monitoring.cluster; | |||
import org.sonar.api.SonarQubeSide; | |||
import org.sonar.api.SonarRuntime; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.process.systeminfo.SystemInfoSection; | |||
import org.sonar.process.systeminfo.SystemInfoUtils; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.platform.ServerLogging; | |||
@ComputeEngineSide | |||
@ServerSide | |||
public class LoggingSection implements SystemInfoSection { | |||
private final SonarRuntime runtime; | |||
private final ServerLogging logging; | |||
public LoggingSection(SonarRuntime runtime, ServerLogging logging) { | |||
this.runtime = runtime; | |||
this.logging = logging; | |||
} | |||
@Override | |||
public ProtobufSystemInfo.Section toProtobuf() { | |||
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); | |||
if (runtime.getSonarQubeSide() == SonarQubeSide.COMPUTE_ENGINE) { | |||
protobuf.setName("Compute Engine Logging"); | |||
} else { | |||
protobuf.setName("Web Logging"); | |||
} | |||
SystemInfoUtils.setAttribute(protobuf, "Logs Level", logging.getRootLoggerLevel().name()); | |||
SystemInfoUtils.setAttribute(protobuf, "Logs Dir", logging.getLogsDir().getAbsolutePath()); | |||
return protobuf.build(); | |||
} | |||
} |
@@ -20,15 +20,23 @@ | |||
package org.sonar.server.platform.monitoring.cluster; | |||
import java.util.ArrayList; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
/** | |||
* Represents the system information of a cluster node. In the case of | |||
* application node, it merges information from Web Server and Compute | |||
* Engine processes. | |||
* | |||
*/ | |||
public class NodeInfo { | |||
private final String name; | |||
private final Map<String, String> attributes = new LinkedHashMap<>(); | |||
private String host = null; | |||
private Long startedAt = null; | |||
private String errorMessage = null; | |||
private final List<ProtobufSystemInfo.Section> sections = new ArrayList<>(); | |||
public NodeInfo(String name) { | |||
@@ -39,13 +47,28 @@ public class NodeInfo { | |||
return name; | |||
} | |||
public NodeInfo setAttribute(String key, String value) { | |||
this.attributes.put(key, value); | |||
return this; | |||
public Optional<String> getHost() { | |||
return Optional.ofNullable(host); | |||
} | |||
public void setHost(@Nullable String s) { | |||
this.host = s; | |||
} | |||
public Optional<Long> getStartedAt() { | |||
return Optional.ofNullable(startedAt); | |||
} | |||
public void setStartedAt(@Nullable Long l) { | |||
this.startedAt = l; | |||
} | |||
public Optional<String> getErrorMessage() { | |||
return Optional.ofNullable(errorMessage); | |||
} | |||
public Map<String, String> getAttributes() { | |||
return attributes; | |||
public void setErrorMessage(@Nullable String s) { | |||
this.errorMessage = s; | |||
} | |||
public NodeInfo addSection(ProtobufSystemInfo.Section section) { |
@@ -25,9 +25,6 @@ import org.sonar.api.server.ServerSide; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.systeminfo.SystemInfoSection; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.health.Health; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.platform.ServerLogging; | |||
import org.sonar.server.platform.monitoring.OfficialDistribution; | |||
import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; | |||
@@ -37,17 +34,12 @@ public class NodeSystemSection implements SystemInfoSection { | |||
private final Configuration config; | |||
private final Server server; | |||
private final ServerLogging serverLogging; | |||
private final OfficialDistribution officialDistribution; | |||
private final HealthChecker healthChecker; | |||
public NodeSystemSection(Configuration config, Server server, ServerLogging serverLogging, | |||
OfficialDistribution officialDistribution, HealthChecker healthChecker) { | |||
public NodeSystemSection(Configuration config, Server server, OfficialDistribution officialDistribution) { | |||
this.config = config; | |||
this.server = server; | |||
this.serverLogging = serverLogging; | |||
this.officialDistribution = officialDistribution; | |||
this.healthChecker = healthChecker; | |||
} | |||
@Override | |||
@@ -55,16 +47,12 @@ public class NodeSystemSection implements SystemInfoSection { | |||
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); | |||
protobuf.setName("System"); | |||
Health health = healthChecker.checkNode(); | |||
setAttribute(protobuf, "Health", health.getStatus().name()); | |||
setAttribute(protobuf, "Health Causes", health.getCauses()); | |||
setAttribute(protobuf, "Version", server.getVersion()); | |||
setAttribute(protobuf, "Official Distribution", officialDistribution.check()); | |||
setAttribute(protobuf, "Home Dir", config.get(ProcessProperties.PATH_HOME).orElse(null)); | |||
setAttribute(protobuf, "Data Dir", config.get(ProcessProperties.PATH_DATA).orElse(null)); | |||
setAttribute(protobuf, "Temp Dir", config.get(ProcessProperties.PATH_TEMP).orElse(null)); | |||
setAttribute(protobuf, "Logs Dir", config.get(ProcessProperties.PATH_LOGS).orElse(null)); | |||
setAttribute(protobuf, "Logs Level", serverLogging.getRootLoggerLevel().name()); | |||
setAttribute(protobuf, "Processors", Runtime.getRuntime().availableProcessors()); | |||
return protobuf.build(); | |||
} | |||
@@ -17,25 +17,14 @@ | |||
* 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.ce.monitoring; | |||
package org.sonar.server.platform.monitoring.cluster; | |||
import org.sonar.process.systeminfo.JvmPropertiesSection; | |||
import org.sonar.process.systeminfo.JvmStateSection; | |||
import org.sonar.server.platform.monitoring.DatabaseSection; | |||
import org.sonar.server.platform.monitoring.cluster.ProcessInfoProvider; | |||
public class CeSystemInfoModule { | |||
import java.util.Collection; | |||
private CeSystemInfoModule() { | |||
// do not instantiate | |||
} | |||
public static Object[] forClusterMode() { | |||
return new Object[] { | |||
new JvmPropertiesSection("Compute Engine JVM Properties"), | |||
new JvmStateSection("Compute Engine JVM State"), | |||
DatabaseSection.class, | |||
ProcessInfoProvider.class | |||
}; | |||
} | |||
/** | |||
* Loads "system information" of all Elasticsearch nodes. | |||
*/ | |||
public interface SearchNodesInfoLoader { | |||
Collection<NodeInfo> load(); | |||
} |
@@ -0,0 +1,66 @@ | |||
/* | |||
* 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.server.platform.monitoring.cluster; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; | |||
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.es.EsClient; | |||
import org.sonar.server.platform.monitoring.EsStateSection; | |||
@ServerSide | |||
public class SearchNodesInfoLoaderImpl implements SearchNodesInfoLoader { | |||
private final EsClient esClient; | |||
public SearchNodesInfoLoaderImpl(EsClient esClient) { | |||
this.esClient = esClient; | |||
} | |||
public Collection<NodeInfo> load() { | |||
NodesStatsResponse nodesStats = esClient.prepareNodesStats() | |||
.setFs(true) | |||
.setProcess(true) | |||
.setJvm(true) | |||
.setIndices(true) | |||
.setBreaker(true) | |||
.get(); | |||
List<NodeInfo> result = new ArrayList<>(); | |||
nodesStats.getNodes().forEach(nodeStat -> result.add(toNodeInfo(nodeStat))); | |||
return result; | |||
} | |||
private static NodeInfo toNodeInfo(NodeStats stat) { | |||
String nodeName = stat.getNode().getName(); | |||
NodeInfo info = new NodeInfo(nodeName); | |||
ProtobufSystemInfo.Section.Builder section = ProtobufSystemInfo.Section.newBuilder(); | |||
section.setName("Search State"); | |||
EsStateSection.toProtobuf(stat, section); | |||
info.addSection(section.build()); | |||
return info; | |||
} | |||
} |
@@ -114,7 +114,6 @@ import org.sonar.server.platform.web.requestid.HttpRequestIdModule; | |||
import org.sonar.server.platform.ws.ChangeLogLevelAction; | |||
import org.sonar.server.platform.ws.DbMigrationStatusAction; | |||
import org.sonar.server.platform.ws.HealthActionModule; | |||
import org.sonar.server.platform.ws.InfoAction; | |||
import org.sonar.server.platform.ws.L10nWs; | |||
import org.sonar.server.platform.ws.LogsAction; | |||
import org.sonar.server.platform.ws.MigrateDbAction; | |||
@@ -542,7 +541,6 @@ public class PlatformLevel4 extends PlatformLevel { | |||
addIfStartupLeader(TelemetryDaemon.class, TelemetryClient.class); | |||
// system info | |||
add(InfoAction.class); | |||
addIfCluster(WebSystemInfoModule.forClusterMode()).otherwiseAdd(WebSystemInfoModule.forStandaloneMode()); | |||
addAll(level4AddedComponents); |
@@ -0,0 +1,101 @@ | |||
/* | |||
* 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.server.platform.ws; | |||
import java.util.Collection; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.process.systeminfo.SystemInfoUtils; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.user.UserSession; | |||
public abstract class BaseInfoWsAction implements SystemWsAction { | |||
private static final String[] ORDERED_SECTION_NAMES = { | |||
"System", "Database", "Plugins", | |||
"Web JVM State", "Web Logging", "Web JVM Properties", | |||
"Search State", "Search Statistics", | |||
"Compute Engine Database Connection", "Compute Engine JVM State", "Compute Engine Logging", "Compute Engine Tasks", "Compute Engine JVM Properties"}; | |||
private final UserSession userSession; | |||
public BaseInfoWsAction(UserSession userSession) { | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller.createAction("info") | |||
.setDescription("Get detailed information about system configuration.<br/>" + | |||
"Requires 'Administer' permissions.<br/>" + | |||
"Since 5.5, this web service becomes internal in order to more easily update result.") | |||
.setSince("5.1") | |||
.setInternal(true) | |||
.setResponseExample(getClass().getResource("/org/sonar/server/platform/ws/info-example.json")) | |||
.setHandler(this); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
userSession.checkIsSystemAdministrator(); | |||
doHandle(request, response); | |||
} | |||
protected abstract void doHandle(Request request, Response response); | |||
protected void writeSectionsToJson(Collection<ProtobufSystemInfo.Section> sections, JsonWriter json) { | |||
SystemInfoUtils | |||
.order(sections, ORDERED_SECTION_NAMES) | |||
.forEach(section -> writeSectionToJson(section, json)); | |||
} | |||
protected void writeSectionToJson(ProtobufSystemInfo.Section section, JsonWriter json) { | |||
json.name(section.getName()); | |||
json.beginObject(); | |||
for (ProtobufSystemInfo.Attribute attribute : section.getAttributesList()) { | |||
writeAttributeToJson(attribute, json); | |||
} | |||
json.endObject(); | |||
} | |||
protected void writeAttributeToJson(ProtobufSystemInfo.Attribute attribute, JsonWriter json) { | |||
switch (attribute.getValueCase()) { | |||
case BOOLEAN_VALUE: | |||
json.prop(attribute.getKey(), attribute.getBooleanValue()); | |||
break; | |||
case LONG_VALUE: | |||
json.prop(attribute.getKey(), attribute.getLongValue()); | |||
break; | |||
case DOUBLE_VALUE: | |||
json.prop(attribute.getKey(), attribute.getDoubleValue()); | |||
break; | |||
case STRING_VALUE: | |||
json.prop(attribute.getKey(), attribute.getStringValue()); | |||
break; | |||
case VALUE_NOT_SET: | |||
json.name(attribute.getKey()).beginArray().values(attribute.getStringValuesList()).endArray(); | |||
break; | |||
default: | |||
throw new IllegalArgumentException("Unsupported type: " + attribute.getValueCase()); | |||
} | |||
} | |||
} |
@@ -22,236 +22,82 @@ package org.sonar.server.platform.ws; | |||
import java.util.Collection; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.health.ClusterHealth; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.platform.monitoring.cluster.AppNodesInfoLoader; | |||
import org.sonar.server.platform.monitoring.cluster.GlobalInfoLoader; | |||
import org.sonar.server.platform.monitoring.cluster.NodeInfo; | |||
import org.sonar.server.platform.monitoring.cluster.SearchNodesInfoLoader; | |||
import org.sonar.server.user.UserSession; | |||
public class ClusterInfoAction implements SystemWsAction { | |||
public class ClusterInfoAction extends BaseInfoWsAction { | |||
private final UserSession userSession; | |||
private final GlobalInfoLoader globalInfoLoader; | |||
private final AppNodesInfoLoader appNodesInfoLoader; | |||
private final SearchNodesInfoLoader searchNodesInfoLoader; | |||
private final HealthChecker healthChecker; | |||
public ClusterInfoAction(UserSession userSession, GlobalInfoLoader globalInfoLoader, AppNodesInfoLoader appNodesInfoLoader) { | |||
this.userSession = userSession; | |||
public ClusterInfoAction(UserSession userSession, GlobalInfoLoader globalInfoLoader, | |||
AppNodesInfoLoader appNodesInfoLoader, SearchNodesInfoLoader searchNodesInfoLoader, HealthChecker healthChecker) { | |||
super(userSession); | |||
this.globalInfoLoader = globalInfoLoader; | |||
this.appNodesInfoLoader = appNodesInfoLoader; | |||
this.searchNodesInfoLoader = searchNodesInfoLoader; | |||
this.healthChecker = healthChecker; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller.createAction("cluster_info") | |||
.setDescription("WIP") | |||
.setSince("6.6") | |||
.setInternal(true) | |||
.setResponseExample(getClass().getResource("/org/sonar/server/platform/ws/info-example.json")) | |||
.setHandler(this); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
userSession.checkIsSystemAdministrator(); | |||
protected void doHandle(Request request, Response response) { | |||
ClusterHealth clusterHealth = healthChecker.checkCluster(); | |||
try (JsonWriter json = response.newJsonWriter()) { | |||
json.beginObject(); | |||
writeGlobal(json); | |||
writeApplicationNodes(json); | |||
json.prop("Health", clusterHealth.getHealth().getStatus().name()); | |||
json.name("Health Causes").beginArray().values(clusterHealth.getHealth().getCauses()).endArray(); | |||
writeGlobalSections(json); | |||
writeApplicationNodes(json, clusterHealth); | |||
writeSearchNodes(json, clusterHealth); | |||
json.endObject(); | |||
} | |||
// try (JsonWriter json = response.newJsonWriter()) { | |||
// json.beginObject(); | |||
// | |||
// // global section | |||
// json.prop("Cluster", true); | |||
// json.prop("Cluster Name", "foo"); | |||
// json.prop("Server Id", "ABC123"); | |||
// json.prop("Health", "RED"); | |||
// json | |||
// .name("Health Causes") | |||
// .beginArray().beginObject().prop("message", "Requires at least two search nodes").endObject().endArray(); | |||
// | |||
// json.name("Settings"); | |||
// json.beginObject(); | |||
// json.prop("sonar.forceAuthentication", true); | |||
// json.prop("sonar.externalIdentityProviders", "GitHub, BitBucket"); | |||
// json.endObject(); | |||
// | |||
// json.name("Database"); | |||
// json | |||
// .beginObject() | |||
// .prop("Name", "PostgreSQL") | |||
// .prop("Version", "9.6.3") | |||
// .endObject(); | |||
// | |||
// json.name("Compute Engine"); | |||
// json | |||
// .beginObject() | |||
// .prop("Pending", 5) | |||
// .prop("In Progress", 4) | |||
// .prop("workers", 8) | |||
// .prop("workersPerNode", 4) | |||
// .endObject(); | |||
// | |||
// json.name("Elasticsearch"); | |||
// json | |||
// .beginObject() | |||
// .prop("Health", "GREEN") | |||
// .prop("Number of Nodes", 4) | |||
// .prop("Index Components - Docs", 152_515_155) | |||
// .prop("Index Components - Shards", 20) | |||
// .prop("Index Components - Size", "25GB") | |||
// .prop("Index Issues - Docs", 5) | |||
// .prop("Index Issues - Shards", 5) | |||
// .prop("Index Issues - Size", "52MB") | |||
// .prop("Index Tests - Docs", 56605) | |||
// .prop("Index Tests - Shards", 2) | |||
// .prop("Index Tests - Size", "520MB") | |||
// .endObject(); | |||
// | |||
// json.name("Application Nodes"); | |||
// json | |||
// .beginArray() | |||
// .beginObject() | |||
// .prop("Name", "Mont Blanc") | |||
// .prop("Host", "10.158.92.16") | |||
// .prop("Health", "YELLOW") | |||
// .name("healthCauses").beginArray().beginObject().prop("message", "Db connectivity error").endObject().endArray() | |||
// .prop("Start Time", "2017-05-30T10:23:45") | |||
// .prop("Official Distribution", true) | |||
// .prop("Processors", 4); | |||
// json | |||
// .name("Web JVM").beginObject() | |||
// .prop("JVM Name", "Java HotSpot(TM) 64-Bit Server VM") | |||
// .prop("JVM Vendor", "Oracle Corporation") | |||
// .prop("Max Memory", "948MB") | |||
// .prop("Free Memory", "38MB") | |||
// .endObject() | |||
// | |||
// .name("Web JVM Properties").beginObject() | |||
// .prop("catalina.home", "/sonarsource/var/tmp/sonarsource/sssonarqube/tc") | |||
// .prop("glowroot.tmp.dir", "/var/tmp/sonarsource/ssglowroot-agent") | |||
// .prop("glowroot.adad.dir", "/var/tmp/sonarsource/ssglowroot-agent") | |||
// .prop("java.specification.version", "1.8") | |||
// .endObject() | |||
// | |||
// .name("Web Database Connectivity").beginObject() | |||
// .prop("Driver", "PostgreSQL JDBC Driver") | |||
// .prop("Driver Version", "PostgreSQL JDBC Driver") | |||
// .prop("Pool Idle Connections", 2) | |||
// .prop("Pool Max Connections", 50) | |||
// .prop("URL", "jdbc:postgresql://next-rds.cn6pfc2xc6oq.us-east-1.rds.amazonaws.com/dory") | |||
// .endObject(); | |||
// | |||
// json | |||
// .name("Compute Engine JVM").beginObject() | |||
// .prop("JVM Name", "Java HotSpot(TM) 64-Bit Server VM") | |||
// .prop("JVM Vendor", "Oracle Corporation") | |||
// .prop("Max Memory", "25MB") | |||
// .prop("Free Memory", "8MB") | |||
// .endObject(); | |||
// | |||
// json | |||
// .name("Compute Engine JVM Properties").beginObject() | |||
// .prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.net.preferIPv4Stack", true) | |||
// .prop("java.rmi.server.randomIDs", true) | |||
// .prop("java.specification.version", "1.8") | |||
// .endObject(); | |||
// | |||
// json.endObject().endArray(); | |||
// | |||
// json.name("Search Nodes"); | |||
// json | |||
// .beginArray() | |||
// .beginObject() | |||
// .prop("Name", "Parmelan") | |||
// .prop("Host", "10.158.92.19") | |||
// .prop("Health", "GREEN") | |||
// .name("Health Causes").beginArray().endArray() | |||
// .prop("Start Time", "2017-05-30T10:23:45") | |||
// .prop("Processors", 2) | |||
// .prop("Disk Available", "25GB") | |||
// .prop("JVM Threads", 52) | |||
// | |||
// .name("JVM Properties").beginObject() | |||
// .prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.net.preferIPv4Stack", true) | |||
// .prop("java.rmi.server.randomIDs", true) | |||
// .endObject() | |||
// | |||
// .name("JVM").beginObject() | |||
// .prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
// .prop("java.net.preferIPv4Stack", true) | |||
// .prop("java.rmi.server.randomIDs", true) | |||
// .endObject() | |||
// | |||
// .endObject() | |||
// .endArray(); | |||
// | |||
// json.endObject(); | |||
// } | |||
} | |||
private void writeGlobal(JsonWriter json) { | |||
globalInfoLoader.load().forEach(section -> sectionToJson(section, json)); | |||
private void writeGlobalSections(JsonWriter json) { | |||
globalInfoLoader.load().forEach(section -> writeSectionToJson(section, json)); | |||
} | |||
private void writeApplicationNodes(JsonWriter json) { | |||
private void writeApplicationNodes(JsonWriter json, ClusterHealth clusterHealth) { | |||
json.name("Application Nodes").beginArray(); | |||
Collection<NodeInfo> appNodes = appNodesInfoLoader.load(); | |||
for (NodeInfo applicationNode : appNodes) { | |||
writeApplicationNode(json, applicationNode); | |||
writeNodeInfoToJson(applicationNode, clusterHealth, json); | |||
} | |||
json.endArray(); | |||
} | |||
private void writeApplicationNode(JsonWriter json, NodeInfo applicationNode) { | |||
json.beginObject(); | |||
json.prop("Name", applicationNode.getName()); | |||
applicationNode.getSections().forEach(section -> sectionToJson(section, json)); | |||
json.endObject(); | |||
private void writeSearchNodes(JsonWriter json, ClusterHealth clusterHealth) { | |||
json.name("Search Nodes").beginArray(); | |||
Collection<NodeInfo> searchNodes = searchNodesInfoLoader.load(); | |||
searchNodes.forEach(node -> writeNodeInfoToJson(node, clusterHealth, json)); | |||
json.endArray(); | |||
} | |||
private static void sectionToJson(ProtobufSystemInfo.Section section, JsonWriter json) { | |||
json.name(section.getName()); | |||
private void writeNodeInfoToJson(NodeInfo nodeInfo, ClusterHealth clusterHealth, JsonWriter json) { | |||
json.beginObject(); | |||
for (ProtobufSystemInfo.Attribute attribute : section.getAttributesList()) { | |||
attributeToJson(json, attribute); | |||
} | |||
json.endObject(); | |||
} | |||
json.prop("Name", nodeInfo.getName()); | |||
json.prop("Error", nodeInfo.getErrorMessage().orElse(null)); | |||
json.prop("Host", nodeInfo.getHost().orElse(null)); | |||
json.prop("Started At", nodeInfo.getStartedAt().orElse(null)); | |||
private static void attributeToJson(JsonWriter json, ProtobufSystemInfo.Attribute attribute) { | |||
switch (attribute.getValueCase()) { | |||
case BOOLEAN_VALUE: | |||
json.prop(attribute.getKey(), attribute.getBooleanValue()); | |||
break; | |||
case LONG_VALUE: | |||
json.prop(attribute.getKey(), attribute.getLongValue()); | |||
break; | |||
case DOUBLE_VALUE: | |||
json.prop(attribute.getKey(), attribute.getDoubleValue()); | |||
break; | |||
case STRING_VALUE: | |||
json.prop(attribute.getKey(), attribute.getStringValue()); | |||
break; | |||
case VALUE_NOT_SET: | |||
json.name(attribute.getKey()).beginArray().values(attribute.getStringValuesList()).endArray(); | |||
break; | |||
default: | |||
throw new IllegalArgumentException("Unsupported type: " + attribute.getValueCase()); | |||
} | |||
clusterHealth.getNodeHealth(nodeInfo.getName()).ifPresent(h -> { | |||
json.prop("Health", h.getStatus().name()); | |||
json.name("Health Causes").beginArray().values(h.getCauses()).endArray(); | |||
}); | |||
writeSectionsToJson(nodeInfo.getSections(), json); | |||
json.endObject(); | |||
} | |||
} |
@@ -22,13 +22,13 @@ package org.sonar.server.platform.ws; | |||
import java.util.List; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.ce.http.CeHttpClient; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.process.systeminfo.SystemInfoSection; | |||
import org.sonar.process.systeminfo.SystemInfoUtils; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.health.Health; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.telemetry.TelemetryDataLoader; | |||
import org.sonar.server.user.UserSession; | |||
@@ -38,39 +38,24 @@ import static org.sonar.server.telemetry.TelemetryDataJsonWriter.writeTelemetryD | |||
/** | |||
* Implementation of the {@code info} action for the System WebService. | |||
*/ | |||
public class InfoAction implements SystemWsAction { | |||
public class InfoAction extends BaseInfoWsAction { | |||
private static final String[] ORDERED_SECTION_NAMES = { | |||
"System", "Database", "Web JVM Properties", "Web JVM State", "Search State", "Search Statistics", | |||
"Compute Engine Database Connection", "Compute Engine JVM State", "Compute Engine Tasks"}; | |||
private final UserSession userSession; | |||
private final CeHttpClient ceHttpClient; | |||
private final SystemInfoSection[] systemInfoSections; | |||
private final HealthChecker healthChecker; | |||
private final TelemetryDataLoader statistics; | |||
public InfoAction(UserSession userSession, CeHttpClient ceHttpClient, TelemetryDataLoader statistics, SystemInfoSection... systemInfoSections) { | |||
this.userSession = userSession; | |||
public InfoAction(UserSession userSession, CeHttpClient ceHttpClient, HealthChecker healthChecker, TelemetryDataLoader statistics, | |||
SystemInfoSection... systemInfoSections) { | |||
super(userSession); | |||
this.ceHttpClient = ceHttpClient; | |||
this.healthChecker = healthChecker; | |||
this.statistics = statistics; | |||
this.systemInfoSections = systemInfoSections; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller.createAction("info") | |||
.setDescription("Get detailed information about system configuration.<br/>" + | |||
"Requires 'Administer' permissions.<br/>" + | |||
"Since 5.5, this web service becomes internal in order to more easily update result.") | |||
.setSince("5.1") | |||
.setInternal(true) | |||
.setResponseExample(getClass().getResource("/org/sonar/server/platform/ws/info-example.json")) | |||
.setHandler(this); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
userSession.checkIsSystemAdministrator(); | |||
protected void doHandle(Request request, Response response) { | |||
try (JsonWriter json = response.newJsonWriter()) { | |||
writeJson(json); | |||
} | |||
@@ -79,53 +64,28 @@ public class InfoAction implements SystemWsAction { | |||
private void writeJson(JsonWriter json) { | |||
json.beginObject(); | |||
writeHealthToJson(json); | |||
List<ProtobufSystemInfo.Section> sections = stream(systemInfoSections) | |||
.map(SystemInfoSection::toProtobuf) | |||
.collect(MoreCollectors.toArrayList()); | |||
ceHttpClient.retrieveSystemInfo() | |||
.ifPresent(ce -> sections.addAll(ce.getSectionsList())); | |||
SystemInfoUtils | |||
.order(sections, ORDERED_SECTION_NAMES) | |||
.forEach(section -> sectionToJson(section, json)); | |||
writeStatistics(json); | |||
writeSectionsToJson(sections, json); | |||
writeStatisticsToJson(json); | |||
json.endObject(); | |||
} | |||
private void writeStatistics(JsonWriter json) { | |||
json.name("Statistics"); | |||
writeTelemetryData(json, statistics.load()); | |||
private void writeHealthToJson(JsonWriter json) { | |||
Health health = healthChecker.checkNode(); | |||
json.prop("Health", health.getStatus().name()); | |||
json.name("Health Causes").beginArray().values(health.getCauses()).endArray(); | |||
} | |||
private static void sectionToJson(ProtobufSystemInfo.Section section, JsonWriter json) { | |||
json.name(section.getName()); | |||
json.beginObject(); | |||
for (ProtobufSystemInfo.Attribute attribute : section.getAttributesList()) { | |||
attributeToJson(json, attribute); | |||
} | |||
json.endObject(); | |||
private void writeStatisticsToJson(JsonWriter json) { | |||
json.name("Statistics"); | |||
writeTelemetryData(json, statistics.load()); | |||
} | |||
private static void attributeToJson(JsonWriter json, ProtobufSystemInfo.Attribute attribute) { | |||
switch (attribute.getValueCase()) { | |||
case BOOLEAN_VALUE: | |||
json.prop(attribute.getKey(), attribute.getBooleanValue()); | |||
break; | |||
case LONG_VALUE: | |||
json.prop(attribute.getKey(), attribute.getLongValue()); | |||
break; | |||
case DOUBLE_VALUE: | |||
json.prop(attribute.getKey(), attribute.getDoubleValue()); | |||
break; | |||
case STRING_VALUE: | |||
json.prop(attribute.getKey(), attribute.getStringValue()); | |||
break; | |||
case VALUE_NOT_SET: | |||
json.name(attribute.getKey()).beginArray().values(attribute.getStringValuesList()).endArray(); | |||
break; | |||
default: | |||
throw new IllegalArgumentException("Unsupported type: " + attribute.getValueCase()); | |||
} | |||
} | |||
} |
@@ -1,168 +0,0 @@ | |||
/* | |||
* 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.server.platform.ws; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.server.user.UserSession; | |||
public class StandaloneInfoAction implements SystemWsAction { | |||
private final UserSession userSession; | |||
public StandaloneInfoAction(UserSession userSession) { | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
controller.createAction("standalone_info") | |||
.setDescription("WIP") | |||
.setSince("6.6") | |||
.setInternal(true) | |||
.setResponseExample(getClass().getResource("/org/sonar/server/platform/ws/info-example.json")) | |||
.setHandler(this); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) { | |||
userSession.checkIsSystemAdministrator(); | |||
try (JsonWriter json = response.newJsonWriter()) { | |||
json.beginObject(); | |||
// global section | |||
json | |||
.prop("Server Id", "ABC123") | |||
.prop("Health", "RED") | |||
.prop("Host", "10.158.92.16") | |||
.prop("Start Time", "2017-05-30T10:23:45") | |||
.prop("Official Distribution", true) | |||
.prop("Processors", 4) | |||
.prop("Disk Available", "25GB") | |||
.prop("JVM Threads", 52); | |||
json | |||
.name("Health Causes") | |||
.beginArray().beginObject().prop("message", "Db connectivity error").endObject().endArray(); | |||
json.name("Settings"); | |||
json.beginObject(); | |||
json.prop("sonar.forceAuthentication", true); | |||
json.prop("sonar.externalIdentityProviders", "GitHub, BitBucket"); | |||
json.endObject(); | |||
json.name("Database"); | |||
json | |||
.beginObject() | |||
.prop("Name", "PostgreSQL") | |||
.prop("Version", "9.6.3") | |||
.endObject(); | |||
json.name("Compute Engine"); | |||
json | |||
.beginObject() | |||
.prop("Pending", 5) | |||
.prop("In Progress", 4) | |||
.prop("workers", 8) | |||
.prop("workersPerNode", 4) | |||
.endObject(); | |||
json.name("Elasticsearch"); | |||
json | |||
.beginObject() | |||
.prop("Health", "GREEN") | |||
.prop("Number of Nodes", 4) | |||
.prop("Index Components - Docs", 152_515_155) | |||
.prop("Index Components - Shards", 20) | |||
.prop("Index Components - Size", "25GB") | |||
.prop("Index Issues - Docs", 5) | |||
.prop("Index Issues - Shards", 5) | |||
.prop("Index Issues - Size", "52MB") | |||
.prop("Index Tests - Docs", 56605) | |||
.prop("Index Tests - Shards", 2) | |||
.prop("Index Tests - Size", "520MB") | |||
.endObject(); | |||
json | |||
.name("Web JVM").beginObject() | |||
.prop("JVM Name", "Java HotSpot(TM) 64-Bit Server VM") | |||
.prop("JVM Vendor", "Oracle Corporation") | |||
.prop("Max Memory", "948MB") | |||
.prop("Free Memory", "38MB") | |||
.endObject() | |||
.name("Web JVM Properties").beginObject() | |||
.prop("catalina.home", "/sonarsource/var/tmp/sonarsource/sssonarqube/tc") | |||
.prop("glowroot.tmp.dir", "/var/tmp/sonarsource/ssglowroot-agent") | |||
.prop("glowroot.adad.dir", "/var/tmp/sonarsource/ssglowroot-agent") | |||
.prop("java.specification.version", "1.8") | |||
.endObject() | |||
.name("Web Database Connectivity").beginObject() | |||
.prop("Driver", "PostgreSQL JDBC Driver") | |||
.prop("Driver Version", "PostgreSQL JDBC Driver") | |||
.prop("Pool Idle Connections", 2) | |||
.prop("Pool Max Connections", 50) | |||
.prop("URL", "jdbc:postgresql://next-rds.cn6pfc2xc6oq.us-east-1.rds.amazonaws.com/dory") | |||
.endObject(); | |||
json | |||
.name("Compute Engine JVM").beginObject() | |||
.prop("JVM Name", "Java HotSpot(TM) 64-Bit Server VM") | |||
.prop("JVM Vendor", "Oracle Corporation") | |||
.prop("Max Memory", "25MB") | |||
.prop("Free Memory", "8MB") | |||
.endObject(); | |||
json | |||
.name("Compute Engine JVM Properties").beginObject() | |||
.prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.net.preferIPv4Stack", true) | |||
.prop("java.rmi.server.randomIDs", true) | |||
.prop("java.specification.version", "1.8") | |||
.endObject(); | |||
json | |||
.name("Search JVM Properties").beginObject() | |||
.prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.net.preferIPv4Stack", true) | |||
.prop("java.rmi.server.randomIDs", true) | |||
.endObject(); | |||
json.name("Search JVM").beginObject() | |||
.prop("java.ext.dirs", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.io.tmpdir", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.library.path", "/opt/sonarsource/jvm/java-1.8.0-sun-x64/jre/lib/ext:/usr/java/packages/lib/ext") | |||
.prop("java.net.preferIPv4Stack", true) | |||
.prop("java.rmi.server.randomIDs", true) | |||
.endObject(); | |||
json.endObject(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,106 @@ | |||
/* | |||
* 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.server.cluster; | |||
import java.net.InetAddress; | |||
import java.net.UnknownHostException; | |||
import java.util.function.Supplier; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.process.NetworkUtils; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.junit.Assert.fail; | |||
import static org.mockito.Matchers.anyString; | |||
import static org.mockito.Mockito.doThrow; | |||
import static org.mockito.Mockito.mock; | |||
public class StartableHazelcastMemberTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private MapSettings settings = new MapSettings(); | |||
private String loopback = InetAddress.getLoopbackAddress().getHostAddress(); | |||
@Test | |||
public void start_initializes_hazelcast() { | |||
completeValidSettings(); | |||
StartableHazelcastMember underTest = new StartableHazelcastMember(settings.asConfig(), NetworkUtils.INSTANCE); | |||
verifyStopped(underTest); | |||
underTest.start(); | |||
assertThat(underTest.getUuid()).isNotEmpty(); | |||
assertThat(underTest.getCluster().getMembers()).hasSize(1); | |||
assertThat(underTest.getMemberUuids()).containsExactly(underTest.getUuid()); | |||
assertThat(underTest.getSet("foo")).isNotNull(); | |||
assertThat(underTest.getReplicatedMap("foo")).isNotNull(); | |||
assertThat(underTest.getAtomicReference("foo")).isNotNull(); | |||
assertThat(underTest.getList("foo")).isNotNull(); | |||
assertThat(underTest.getMap("foo")).isNotNull(); | |||
assertThat(underTest.getLock("foo")).isNotNull(); | |||
assertThat(underTest.getClusterTime()).isGreaterThan(0); | |||
underTest.stop(); | |||
verifyStopped(underTest); | |||
} | |||
@Test | |||
public void throw_ISE_if_host_for_random_port_cant_be_resolved() throws Exception{ | |||
NetworkUtils network = mock(NetworkUtils.class); | |||
doThrow(new UnknownHostException("BOOM")).when(network).toInetAddress(anyString()); | |||
completeValidSettings(); | |||
StartableHazelcastMember underTest = new StartableHazelcastMember(settings.asConfig(), network); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Can not resolve address "); | |||
underTest.start(); | |||
verifyStopped(underTest); | |||
} | |||
private void completeValidSettings() { | |||
settings.setProperty("sonar.cluster.name", "foo"); | |||
settings.setProperty("sonar.cluster.node.host", loopback); | |||
settings.setProperty("sonar.cluster.node.name", "bar"); | |||
settings.setProperty("sonar.cluster.node.type", "application"); | |||
settings.setProperty("process.key", "ce"); | |||
} | |||
private static void verifyStopped(StartableHazelcastMember member) { | |||
expectNpe(member::getMemberUuids); | |||
expectNpe(member::getCluster); | |||
expectNpe(member::getUuid); | |||
} | |||
private static void expectNpe(Supplier supplier) { | |||
try { | |||
supplier.get(); | |||
fail(); | |||
} catch (NullPointerException e) { | |||
} | |||
} | |||
} |
@@ -19,7 +19,9 @@ | |||
*/ | |||
package org.sonar.server.health; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.HashSet; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
@@ -100,6 +102,17 @@ public class ClusterHealthTest { | |||
assertThat(underTest.toString()).isEqualTo("ClusterHealth{health=" + health + ", nodes=" + nodeHealths + "}"); | |||
} | |||
@Test | |||
public void test_getNodeHealth() { | |||
Health health = randomHealth(); | |||
Set<NodeHealth> nodeHealths = new HashSet<>(Arrays.asList(newNodeHealth("foo"), newNodeHealth("bar"))); | |||
ClusterHealth underTest = new ClusterHealth(health, nodeHealths); | |||
assertThat(underTest.getNodeHealth("does_not_exist")).isEmpty(); | |||
assertThat(underTest.getNodeHealth("bar")).isPresent(); | |||
} | |||
private Health randomHealth() { | |||
Health.Builder healthBuilder = Health.newHealthCheckBuilder(); | |||
healthBuilder.setStatus(Health.Status.values()[random.nextInt(Health.Status.values().length)]); | |||
@@ -120,4 +133,17 @@ public class ClusterHealthTest { | |||
.build()) | |||
.build()).collect(Collectors.toSet()); | |||
} | |||
private static NodeHealth newNodeHealth(String nodeName) { | |||
return NodeHealth.newNodeHealthBuilder() | |||
.setStatus(NodeHealth.Status.YELLOW) | |||
.setDetails(NodeDetails.newNodeDetailsBuilder() | |||
.setType(NodeDetails.Type.APPLICATION) | |||
.setName(nodeName) | |||
.setHost(randomAlphanumeric(4)) | |||
.setPort(3000) | |||
.setStartedAt(1_000L) | |||
.build()) | |||
.build(); | |||
} | |||
} |
@@ -30,14 +30,11 @@ import org.sonar.api.utils.log.LoggerLevel; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.authentication.IdentityProviderRepositoryRule; | |||
import org.sonar.server.authentication.TestIdentityProvider; | |||
import org.sonar.server.health.Health; | |||
import org.sonar.server.health.TestStandaloneHealthChecker; | |||
import org.sonar.server.platform.ServerId; | |||
import org.sonar.server.platform.ServerIdLoader; | |||
import org.sonar.server.platform.ServerLogging; | |||
import org.sonar.server.user.SecurityRealmFactory; | |||
import static java.util.Arrays.asList; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
@@ -57,11 +54,10 @@ public class StandaloneSystemSectionTest { | |||
private ServerIdLoader serverIdLoader = mock(ServerIdLoader.class); | |||
private ServerLogging serverLogging = mock(ServerLogging.class); | |||
private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); | |||
private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker(); | |||
private OfficialDistribution officialDistribution = mock(OfficialDistribution.class); | |||
private StandaloneSystemSection underTest = new StandaloneSystemSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server, | |||
serverLogging, serverIdLoader, officialDistribution, healthChecker); | |||
serverLogging, serverIdLoader, officialDistribution); | |||
@Before | |||
public void setUp() throws Exception { | |||
@@ -192,19 +188,6 @@ public class StandaloneSystemSectionTest { | |||
assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub"); | |||
} | |||
@Test | |||
public void return_health() { | |||
healthChecker.setHealth(Health.newHealthCheckBuilder() | |||
.setStatus(Health.Status.YELLOW) | |||
.addCause("foo") | |||
.addCause("bar") | |||
.build()); | |||
ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); | |||
assertThatAttributeIs(protobuf, "Health", "YELLOW"); | |||
SystemInfoTesting.assertThatAttributeHasOnlyValues(protobuf, "Health Causes", asList("foo", "bar")); | |||
} | |||
@Test | |||
public void return_nb_of_processors() { | |||
ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); |
@@ -29,6 +29,7 @@ import org.sonar.ce.http.CeHttpClientImpl; | |||
import org.sonar.process.systeminfo.SystemInfoSection; | |||
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.health.TestStandaloneHealthChecker; | |||
import org.sonar.server.telemetry.TelemetryData; | |||
import org.sonar.server.telemetry.TelemetryDataLoader; | |||
import org.sonar.server.tester.UserSessionRule; | |||
@@ -51,9 +52,10 @@ public class InfoActionTest { | |||
private SystemInfoSection section1 = mock(SystemInfoSection.class); | |||
private SystemInfoSection section2 = mock(SystemInfoSection.class); | |||
private CeHttpClient ceHttpClient = mock(CeHttpClientImpl.class, Mockito.RETURNS_MOCKS); | |||
private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker(); | |||
private TelemetryDataLoader statistics = mock(TelemetryDataLoader.class); | |||
private InfoAction underTest = new InfoAction(userSessionRule, ceHttpClient, statistics, section1, section2); | |||
private InfoAction underTest = new InfoAction(userSessionRule, ceHttpClient, healthChecker, statistics, section1, section2); | |||
private WsActionTester ws = new WsActionTester(underTest); | |||
@Test | |||
@@ -100,7 +102,7 @@ public class InfoActionTest { | |||
TestResponse response = ws.newRequest().execute(); | |||
// response does not contain empty "Section Three" | |||
verify(statistics).load(); | |||
assertThat(response.getInput()).isEqualTo("{\"Section One\":{\"foo\":\"bar\"},\"Section Two\":{\"one\":1,\"two\":2}," + | |||
assertThat(response.getInput()).isEqualTo("{\"Health\":\"GREEN\",\"Health Causes\":[],\"Section One\":{\"foo\":\"bar\"},\"Section Two\":{\"one\":1,\"two\":2}," + | |||
"\"Statistics\":{\"plugins\":{},\"userCount\":0,\"projectCount\":0,\"lines\":0,\"ncloc\":0,\"projectCountByLanguage\":{},\"nclocByLanguage\":{}}}"); | |||
} | |||
@@ -26,6 +26,8 @@ import org.sonar.ce.http.CeHttpClient; | |||
import org.sonar.ce.http.CeHttpClientImpl; | |||
import org.sonar.server.app.ProcessCommandWrapper; | |||
import org.sonar.server.app.RestartFlagHolder; | |||
import org.sonar.server.health.HealthChecker; | |||
import org.sonar.server.health.TestStandaloneHealthChecker; | |||
import org.sonar.server.platform.Platform; | |||
import org.sonar.server.platform.WebServer; | |||
import org.sonar.server.telemetry.TelemetryDataLoader; | |||
@@ -37,13 +39,14 @@ import static org.mockito.Mockito.mock; | |||
public class SystemWsTest { | |||
CeHttpClient ceHttpClient = mock(CeHttpClientImpl.class); | |||
private CeHttpClient ceHttpClient = mock(CeHttpClientImpl.class); | |||
private HealthChecker healthChecker = new TestStandaloneHealthChecker(); | |||
@Test | |||
public void define() { | |||
RestartAction action1 = new RestartAction(mock(UserSession.class), mock(Configuration.class), mock(Platform.class), mock(ProcessCommandWrapper.class), | |||
mock(RestartFlagHolder.class), mock(WebServer.class)); | |||
InfoAction action2 = new InfoAction(new AnonymousMockUserSession(), ceHttpClient, mock(TelemetryDataLoader.class)); | |||
InfoAction action2 = new InfoAction(new AnonymousMockUserSession(), ceHttpClient, healthChecker, mock(TelemetryDataLoader.class)); | |||
SystemWs ws = new SystemWs(action1, action2); | |||
WebService.Context context = new WebService.Context(); | |||
@@ -36,21 +36,26 @@ export enum HealthType { | |||
GREEN = 'GREEN' | |||
} | |||
export interface HealthCause extends SysValueObject { | |||
message: string; | |||
} | |||
export interface NodeInfo extends SysValueObject { | |||
Name: string; | |||
'Compute Engine Logging': { 'Logs Level': string }; | |||
Health: HealthType; | |||
'Health Causes': HealthCause[]; | |||
'Logs Level': string; | |||
'Health Causes': string[]; | |||
Name: string; | |||
'Web Logging': { 'Logs Level': string }; | |||
} | |||
export interface SysInfo extends SysValueObject { | |||
Cluster: boolean; | |||
Health: HealthType; | |||
'Health Causes': HealthCause[]; | |||
'Health Causes': string[]; | |||
System: { | |||
'High Availability': boolean; | |||
'Logs Level': string; | |||
}; | |||
} | |||
export interface ClusterSysInfo extends SysInfo { | |||
'Application Nodes': NodeInfo[]; | |||
'Search Nodes': NodeInfo[]; | |||
} | |||
export function setLogLevel(level: string): Promise<void | Response> { |
@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as u from '../utils'; | |||
import { ClusterSysInfo, SysInfo } from '../../../api/system'; | |||
describe('parseQuery', () => { | |||
it('should correctly parse the expand array', () => { | |||
@@ -44,16 +45,26 @@ describe('groupSections', () => { | |||
describe('getSystemLogsLevel', () => { | |||
it('should correctly return log level for standalone mode', () => { | |||
expect(u.getSystemLogsLevel({ 'Logs Level': 'FOO' } as u.StandaloneSysInfo)).toBe('FOO'); | |||
expect(u.getSystemLogsLevel({} as u.StandaloneSysInfo)).toBe('INFO'); | |||
expect(u.getSystemLogsLevel({ System: { 'Logs Level': 'FOO' } } as SysInfo)).toBe('FOO'); | |||
expect(u.getSystemLogsLevel({} as SysInfo)).toBe('INFO'); | |||
expect(u.getSystemLogsLevel()).toBe('INFO'); | |||
}); | |||
it('should return the worst log level for cluster mode', () => { | |||
expect( | |||
u.getSystemLogsLevel({ | |||
Cluster: true, | |||
'Application Nodes': [{ 'Logs Level': 'INFO' }, { 'Logs Level': 'DEBUG' }] | |||
} as u.ClusterSysInfo) | |||
System: { 'High Availability': true }, | |||
'Application Nodes': [ | |||
{ | |||
'Compute Engine Logging': { 'Logs Level': 'DEBUG' }, | |||
'Web Logging': { 'Logs Level': 'INFO' } | |||
}, | |||
{ | |||
'Compute Engine Logging': { 'Logs Level': 'INFO' }, | |||
'Web Logging': { 'Logs Level': 'INFO' } | |||
} | |||
] | |||
} as ClusterSysInfo) | |||
).toBe('DEBUG'); | |||
}); | |||
}); |
@@ -24,16 +24,8 @@ import ClusterSysInfos from './ClusterSysInfos'; | |||
import PageHeader from './PageHeader'; | |||
import StandaloneSysInfos from './StandaloneSysInfos'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getSystemInfo, SysInfo } from '../../../api/system'; | |||
import { | |||
ClusterSysInfo, | |||
getSystemLogsLevel, | |||
isCluster, | |||
parseQuery, | |||
Query, | |||
serializeQuery, | |||
StandaloneSysInfo | |||
} from '../utils'; | |||
import { ClusterSysInfo, getSystemInfo, SysInfo } from '../../../api/system'; | |||
import { getSystemLogsLevel, isCluster, parseQuery, Query, serializeQuery } from '../utils'; | |||
import { RawQuery } from '../../../helpers/query'; | |||
import '../styles.css'; | |||
@@ -114,7 +106,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
return ( | |||
<StandaloneSysInfos | |||
expandedCards={query.expandedCards} | |||
sysInfoData={sysInfoData as StandaloneSysInfo} | |||
sysInfoData={sysInfoData} | |||
toggleCard={this.toggleSysInfoCards} | |||
/> | |||
); |
@@ -49,7 +49,7 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State | |||
handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { newLevel } = this.state; | |||
if (!this.state.updating && newLevel !== this.props.logLevel) { | |||
if (!this.state.updating) { | |||
this.setState({ updating: true }); | |||
setLogLevel(newLevel).then( | |||
() => this.props.onChange(newLevel), | |||
@@ -64,7 +64,6 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State | |||
render() { | |||
const { updating, newLevel } = this.state; | |||
const header = translate('system.set_log_level'); | |||
const disableSubmit = updating || newLevel === this.props.logLevel; | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
@@ -100,7 +99,7 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State | |||
</div> | |||
<div className="modal-foot"> | |||
{updating && <i className="spinner spacer-right" />} | |||
<button disabled={disableSubmit} id="set-log-level-submit"> | |||
<button disabled={updating} id="set-log-level-submit"> | |||
{translate('save')} | |||
</button> | |||
<a href="#" id="set-log-level-cancel" onClick={this.handleCancelClick}> |
@@ -21,8 +21,8 @@ import * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import HealthCard from './info-items/HealthCard'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { ClusterSysInfo } from '../../../api/system'; | |||
import { | |||
ClusterSysInfo, | |||
getAppNodes, | |||
getHealth, | |||
getHealthCauses, |
@@ -20,18 +20,18 @@ | |||
import * as React from 'react'; | |||
import { map } from 'lodash'; | |||
import HealthCard from './info-items/HealthCard'; | |||
import { SysInfo } from '../../../api/system'; | |||
import { | |||
getHealth, | |||
getHealthCauses, | |||
getStandaloneMainSections, | |||
getStandaloneSecondarySections, | |||
ignoreInfoFields, | |||
StandaloneSysInfo | |||
ignoreInfoFields | |||
} from '../utils'; | |||
interface Props { | |||
expandedCards: string[]; | |||
sysInfoData: StandaloneSysInfo; | |||
sysInfoData: SysInfo; | |||
toggleCard: (toggledCard: string) => void; | |||
} | |||
@@ -20,20 +20,33 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ClusterSysInfos from '../ClusterSysInfos'; | |||
import { HealthType } from '../../../../api/system'; | |||
import { ClusterSysInfo } from '../../utils'; | |||
import { ClusterSysInfo, HealthType } from '../../../../api/system'; | |||
const sysInfoData: ClusterSysInfo = { | |||
Cluster: true, | |||
Health: HealthType.RED, | |||
Name: 'Foo', | |||
'Health Causes': [{ message: 'Database down' }], | |||
'Health Causes': ['Database down'], | |||
'Application Nodes': [ | |||
{ Name: 'Bar', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' } | |||
{ | |||
Name: 'Bar', | |||
Health: HealthType.GREEN, | |||
'Health Causes': [], | |||
'Compute Engine Logging': { 'Logs Level': 'INFO' }, | |||
'Web Logging': { 'Logs Level': 'INFO' } | |||
} | |||
], | |||
'Search Nodes': [ | |||
{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'INFO' } | |||
] | |||
{ | |||
Name: 'Baz', | |||
Health: HealthType.YELLOW, | |||
'Health Causes': [], | |||
'Compute Engine Logging': { 'Logs Level': 'INFO' }, | |||
'Web Logging': { 'Logs Level': 'INFO' } | |||
} | |||
], | |||
System: { | |||
'High Availability': true, | |||
'Logs Level': 'INFO' | |||
} | |||
}; | |||
it('should render correctly', () => { | |||
@@ -42,9 +55,27 @@ it('should render correctly', () => { | |||
sysInfoData: { | |||
...sysInfoData, | |||
'Application Nodes': [ | |||
{ Name: 'Foo', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' }, | |||
{ Name: 'Bar', Health: HealthType.RED, 'Health Causes': [], 'Logs Level': 'DEBUG' }, | |||
{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'TRACE' } | |||
{ | |||
Name: 'Foo', | |||
Health: HealthType.GREEN, | |||
'Health Causes': [], | |||
'Compute Engine Logging': { 'Logs Level': 'INFO' }, | |||
'Web Logging': { 'Logs Level': 'INFO' } | |||
}, | |||
{ | |||
Name: 'Bar', | |||
Health: HealthType.RED, | |||
'Health Causes': [], | |||
'Compute Engine Logging': { 'Logs Level': 'INFO' }, | |||
'Web Logging': { 'Logs Level': 'DEBUG' } | |||
}, | |||
{ | |||
Name: 'Baz', | |||
Health: HealthType.YELLOW, | |||
'Health Causes': [], | |||
'Compute Engine Logging': { 'Logs Level': 'TRACE' }, | |||
'Web Logging': { 'Logs Level': 'DEBUG' } | |||
} | |||
] | |||
} | |||
}).find('HealthCard') |
@@ -20,18 +20,18 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import StandaloneSysInfos from '../StandaloneSysInfos'; | |||
import { HealthType } from '../../../../api/system'; | |||
import { StandaloneSysInfo } from '../../utils'; | |||
import { HealthType, SysInfo } from '../../../../api/system'; | |||
const sysInfoData: StandaloneSysInfo = { | |||
Cluster: true, | |||
const sysInfoData: SysInfo = { | |||
Health: HealthType.RED, | |||
'Logs Level': 'DEBUG', | |||
Name: 'Foo', | |||
'Health Causes': [{ message: 'Database down' }], | |||
'Health Causes': ['Database down'], | |||
'Web JVM': { 'Max Memory': '2Gb' }, | |||
'Compute Engine': { Pending: 4 }, | |||
Elasticsearch: { 'Number of Nodes': 1 } | |||
Search: { 'Number of Nodes': 1 }, | |||
System: { | |||
'High Availability': true, | |||
'Logs Level': 'DEBUG' | |||
} | |||
}; | |||
it('should render correctly', () => { |
@@ -97,7 +97,7 @@ exports[`should display some warning messages for non INFO levels 1`] = ` | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={true} | |||
disabled={false} | |||
id="set-log-level-submit" | |||
> | |||
save | |||
@@ -206,7 +206,7 @@ exports[`should render correctly 1`] = ` | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={true} | |||
disabled={false} | |||
id="set-log-level-submit" | |||
> | |||
save |
@@ -7,9 +7,7 @@ exports[`should support more than two nodes 1`] = ` | |||
health="RED" | |||
healthCauses={ | |||
Array [ | |||
Object { | |||
"message": "Database down", | |||
}, | |||
"Database down", | |||
] | |||
} | |||
name="System" | |||
@@ -17,7 +15,8 @@ exports[`should support more than two nodes 1`] = ` | |||
open={true} | |||
sysInfoData={ | |||
Object { | |||
"Name": "Foo", | |||
"High Availability": true, | |||
"Logs Level": "INFO", | |||
} | |||
} | |||
/> | |||
@@ -34,8 +33,12 @@ exports[`should support more than two nodes 1`] = ` | |||
open={false} | |||
sysInfoData={ | |||
Object { | |||
"Logs Level": "INFO", | |||
"Name": "Bar", | |||
"Compute Engine Logging": Object { | |||
"Logs Level": "INFO", | |||
}, | |||
"Web Logging": Object { | |||
"Logs Level": "INFO", | |||
}, | |||
} | |||
} | |||
/> | |||
@@ -52,8 +55,12 @@ exports[`should support more than two nodes 1`] = ` | |||
open={false} | |||
sysInfoData={ | |||
Object { | |||
"Logs Level": "INFO", | |||
"Name": "Baz", | |||
"Compute Engine Logging": Object { | |||
"Logs Level": "INFO", | |||
}, | |||
"Web Logging": Object { | |||
"Logs Level": "INFO", | |||
}, | |||
} | |||
} | |||
/> |
@@ -7,9 +7,7 @@ exports[`should render correctly 1`] = ` | |||
health="RED" | |||
healthCauses={ | |||
Array [ | |||
Object { | |||
"message": "Database down", | |||
}, | |||
"Database down", | |||
] | |||
} | |||
name="System" | |||
@@ -17,8 +15,8 @@ exports[`should render correctly 1`] = ` | |||
open={false} | |||
sysInfoData={ | |||
Object { | |||
"High Availability": true, | |||
"Logs Level": "DEBUG", | |||
"Name": "Foo", | |||
} | |||
} | |||
/> | |||
@@ -28,11 +26,9 @@ exports[`should render correctly 1`] = ` | |||
open={false} | |||
sysInfoData={ | |||
Object { | |||
"Web Database Connectivity": undefined, | |||
"Web JVM": Object { | |||
"Max Memory": "2Gb", | |||
}, | |||
"Web JVM Properties": undefined, | |||
} | |||
} | |||
/> | |||
@@ -42,9 +38,9 @@ exports[`should render correctly 1`] = ` | |||
open={true} | |||
sysInfoData={ | |||
Object { | |||
"Compute Engine JVM": undefined, | |||
"Compute Engine JVM Properties": undefined, | |||
"Pending": 4, | |||
"Compute Engine": Object { | |||
"Pending": 4, | |||
}, | |||
} | |||
} | |||
/> | |||
@@ -54,11 +50,9 @@ exports[`should render correctly 1`] = ` | |||
open={false} | |||
sysInfoData={ | |||
Object { | |||
"Elasticsearch": Object { | |||
"Search": Object { | |||
"Number of Nodes": 1, | |||
}, | |||
"Search JVM": undefined, | |||
"Search JVM Properties": undefined, | |||
} | |||
} | |||
/> |
@@ -23,14 +23,14 @@ import { map } from 'lodash'; | |||
import HealthItem from './HealthItem'; | |||
import OpenCloseIcon from '../../../../components/icons-components/OpenCloseIcon'; | |||
import Section from './Section'; | |||
import { HealthType, HealthCause, SysValueObject } from '../../../../api/system'; | |||
import { HealthType, SysValueObject } from '../../../../api/system'; | |||
import { LOGS_LEVELS, groupSections, getLogsLevel } from '../../utils'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
biggerHealth?: boolean; | |||
health?: HealthType; | |||
healthCauses?: HealthCause[]; | |||
healthCauses?: string[]; | |||
onClick: (toggledCard: string) => void; | |||
open: boolean; | |||
name: string; |
@@ -19,12 +19,12 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { HealthCause, HealthType } from '../../../../api/system'; | |||
import { HealthType } from '../../../../api/system'; | |||
interface Props { | |||
className?: string; | |||
health: HealthType; | |||
healthCause: HealthCause; | |||
healthCause: string; | |||
} | |||
export default function HealthCauseItem({ className, health, healthCause }: Props) { | |||
@@ -35,7 +35,7 @@ export default function HealthCauseItem({ className, health, healthCause }: Prop | |||
health === HealthType.RED ? 'alert-danger' : 'alert-warning', | |||
className | |||
)}> | |||
{healthCause.message} | |||
{healthCause} | |||
</span> | |||
); | |||
} |
@@ -21,13 +21,13 @@ import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import HealthCauseItem from './HealthCauseItem'; | |||
import StatusIndicator from '../../../../components/common/StatusIndicator'; | |||
import { HealthType, HealthCause } from '../../../../api/system'; | |||
import { HealthType } from '../../../../api/system'; | |||
interface Props { | |||
biggerHealth?: boolean; | |||
className?: string; | |||
health: HealthType; | |||
healthCauses?: HealthCause[]; | |||
healthCauses?: string[]; | |||
} | |||
export default function HealthItem({ biggerHealth, className, health, healthCauses }: Props) { |
@@ -59,7 +59,7 @@ function getShallowWrapper(props = {}) { | |||
<HealthCard | |||
biggerHealth={false} | |||
health={HealthType.RED} | |||
healthCauses={[{ message: 'foo' }]} | |||
healthCauses={['foo']} | |||
name="Foobar" | |||
onClick={() => {}} | |||
open={false} |
@@ -23,10 +23,8 @@ import HealthCauseItem from '../HealthCauseItem'; | |||
import { HealthType } from '../../../../../api/system'; | |||
it('should render correctly', () => { | |||
expect(shallow(<HealthCauseItem health={HealthType.RED} healthCause="foo" />)).toMatchSnapshot(); | |||
expect( | |||
shallow(<HealthCauseItem health={HealthType.RED} healthCause={{ message: 'foo' }} />) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow(<HealthCauseItem health={HealthType.YELLOW} healthCause={{ message: 'foo' }} />) | |||
shallow(<HealthCauseItem health={HealthType.YELLOW} healthCause="foo" />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -24,26 +24,19 @@ import { HealthType } from '../../../../../api/system'; | |||
it('should render correctly', () => { | |||
expect( | |||
shallow( | |||
<HealthItem biggerHealth={true} health={HealthType.RED} healthCauses={[{ message: 'foo' }]} /> | |||
) | |||
shallow(<HealthItem biggerHealth={true} health={HealthType.RED} healthCauses={['foo']} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should not render health causes', () => { | |||
expect( | |||
shallow(<HealthItem health={HealthType.GREEN} healthCauses={[{ message: 'foo' }]} />) | |||
shallow(<HealthItem health={HealthType.GREEN} healthCauses={['foo']} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<HealthItem health={HealthType.YELLOW} healthCauses={[]} />)).toMatchSnapshot(); | |||
}); | |||
it('should render multiple health causes', () => { | |||
expect( | |||
shallow( | |||
<HealthItem | |||
health={HealthType.YELLOW} | |||
healthCauses={[{ message: 'foo' }, { message: 'bar' }]} | |||
/> | |||
) | |||
shallow(<HealthItem health={HealthType.YELLOW} healthCauses={['foo', 'bar']} />) | |||
).toMatchSnapshot(); | |||
}); |
@@ -31,9 +31,7 @@ exports[`should display the sysinfo detail 1`] = ` | |||
health="RED" | |||
healthCauses={ | |||
Array [ | |||
Object { | |||
"message": "foo", | |||
}, | |||
"foo", | |||
] | |||
} | |||
/> | |||
@@ -69,9 +67,7 @@ exports[`should render correctly 1`] = ` | |||
health="RED" | |||
healthCauses={ | |||
Array [ | |||
Object { | |||
"message": "foo", | |||
}, | |||
"foo", | |||
] | |||
} | |||
/> | |||
@@ -102,9 +98,7 @@ exports[`should show a main section and multiple sub sections 1`] = ` | |||
health="RED" | |||
healthCauses={ | |||
Array [ | |||
Object { | |||
"message": "foo", | |||
}, | |||
"foo", | |||
] | |||
} | |||
/> |
@@ -27,11 +27,7 @@ exports[`should render correctly 1`] = ` | |||
<HealthCauseItem | |||
className="spacer-right" | |||
health="RED" | |||
healthCause={ | |||
Object { | |||
"message": "foo", | |||
} | |||
} | |||
healthCause="foo" | |||
/> | |||
<StatusIndicator | |||
color="red" | |||
@@ -47,20 +43,12 @@ exports[`should render multiple health causes 1`] = ` | |||
<HealthCauseItem | |||
className="spacer-right" | |||
health="YELLOW" | |||
healthCause={ | |||
Object { | |||
"message": "foo", | |||
} | |||
} | |||
healthCause="foo" | |||
/> | |||
<HealthCauseItem | |||
className="spacer-right" | |||
health="YELLOW" | |||
healthCause={ | |||
Object { | |||
"message": "bar", | |||
} | |||
} | |||
healthCause="bar" | |||
/> | |||
<StatusIndicator | |||
color="yellow" |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { each, omit, memoize, sortBy } from 'lodash'; | |||
import { each, memoize, omit, omitBy, pickBy, sortBy } from 'lodash'; | |||
import { | |||
cleanQuery, | |||
parseAsArray, | |||
@@ -26,7 +26,7 @@ import { | |||
serializeStringArray | |||
} from '../../helpers/query'; | |||
import { | |||
HealthCause, | |||
ClusterSysInfo, | |||
HealthType, | |||
NodeInfo, | |||
SysInfo, | |||
@@ -38,45 +38,64 @@ export interface Query { | |||
expandedCards: string[]; | |||
} | |||
export interface ClusterSysInfo extends SysInfo { | |||
'Application Nodes': NodeInfo[]; | |||
'Search Nodes': NodeInfo[]; | |||
} | |||
export interface StandaloneSysInfo extends SysInfo { | |||
'Logs Level': string; | |||
} | |||
export const LOGS_LEVELS = ['INFO', 'DEBUG', 'TRACE']; | |||
export const HA_FIELD = 'High Availability'; | |||
export const HEALTH_FIELD = 'Health'; | |||
export const HEALTHCAUSES_FIELD = 'Health Causes'; | |||
export function ignoreInfoFields(sysInfoObject: SysValueObject): SysValueObject { | |||
return omit(sysInfoObject, ['Cluster', HEALTH_FIELD, HEALTHCAUSES_FIELD]); | |||
return omit(sysInfoObject, [HEALTH_FIELD, HEALTHCAUSES_FIELD, 'Name', 'Settings']); | |||
} | |||
export function getHealth(sysInfoObject: SysValueObject): HealthType { | |||
return sysInfoObject[HEALTH_FIELD] as HealthType; | |||
} | |||
export function getHealthCauses(sysInfoObject: SysValueObject): HealthCause[] { | |||
return sysInfoObject[HEALTHCAUSES_FIELD] as HealthCause[]; | |||
export function getHealthCauses(sysInfoObject: SysValueObject): string[] { | |||
return sysInfoObject[HEALTHCAUSES_FIELD] as string[]; | |||
} | |||
export function getLogsLevel(sysInfoObject: SysValueObject): string { | |||
if (sysInfoObject['Web Logging']) { | |||
return sortBy( | |||
[ | |||
(sysInfoObject as NodeInfo)['Compute Engine Logging']['Logs Level'], | |||
(sysInfoObject as NodeInfo)['Web Logging']['Logs Level'] | |||
], | |||
logLevel => LOGS_LEVELS.indexOf(logLevel) | |||
)[1]; | |||
} | |||
if (sysInfoObject['System']) { | |||
return (sysInfoObject as SysInfo)['System']['Logs Level']; | |||
} | |||
return (sysInfoObject['Logs Level'] || LOGS_LEVELS[0]) as string; | |||
} | |||
export function getAppNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { | |||
return sysInfoData['Application Nodes']; | |||
} | |||
export function getSearchNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { | |||
return sysInfoData['Search Nodes']; | |||
} | |||
export function isCluster(sysInfoData?: SysInfo): boolean { | |||
return ( | |||
sysInfoData != undefined && sysInfoData['System'] && sysInfoData['System'][HA_FIELD] === true | |||
); | |||
} | |||
export function getSystemLogsLevel(sysInfoData?: SysInfo): string { | |||
const defaultLevel = LOGS_LEVELS[0]; | |||
if (!sysInfoData) { | |||
return defaultLevel; | |||
} | |||
if (isCluster(sysInfoData)) { | |||
const nodes = sortBy(getAppNodes(sysInfoData as ClusterSysInfo), node => | |||
LOGS_LEVELS.indexOf(getLogsLevel(node)) | |||
const logLevels = sortBy( | |||
getAppNodes(sysInfoData as ClusterSysInfo).map(getLogsLevel), | |||
logLevel => LOGS_LEVELS.indexOf(logLevel) | |||
); | |||
return nodes.length > 0 ? getLogsLevel(nodes[nodes.length - 1]) : defaultLevel; | |||
return logLevels.length > 0 ? logLevels[logLevels.length - 1] : defaultLevel; | |||
} else { | |||
return getLogsLevel(sysInfoData); | |||
} | |||
@@ -87,51 +106,40 @@ export function getNodeName(nodeInfo: NodeInfo): string { | |||
} | |||
export function getClusterMainCardSection(sysInfoData: ClusterSysInfo): SysValueObject { | |||
return omit(sysInfoData, ['Application Nodes', 'Search Nodes', 'Settings', 'Statistics']); | |||
} | |||
export function getStandaloneMainSections(sysInfoData: StandaloneSysInfo): SysValueObject { | |||
return omit(sysInfoData, [ | |||
'Settings', | |||
'Statistics', | |||
'Compute Engine', | |||
'Compute Engine JVM', | |||
'Compute Engine JVM Properties', | |||
'Elasticsearch', | |||
'Search JVM', | |||
'Search JVM Properties', | |||
'Web Database Connectivity', | |||
'Web JVM', | |||
'Web JVM Properties' | |||
]); | |||
} | |||
export function getStandaloneSecondarySections(sysInfoData: StandaloneSysInfo): SysInfoSection { | |||
return { | |||
Web: { | |||
'Web Database Connectivity': sysInfoData['Web Database Connectivity'], | |||
'Web JVM': sysInfoData['Web JVM'], | |||
'Web JVM Properties': sysInfoData['Web JVM Properties'] | |||
}, | |||
'Compute Engine': { | |||
...sysInfoData['Compute Engine'] as SysValueObject, | |||
'Compute Engine JVM': sysInfoData['Compute Engine JVM'], | |||
'Compute Engine JVM Properties': sysInfoData['Compute Engine JVM Properties'] | |||
}, | |||
Search: { | |||
Elasticsearch: sysInfoData['Elasticsearch'] as SysValueObject, | |||
'Search JVM': sysInfoData['Search JVM'], | |||
'Search JVM Properties': sysInfoData['Search JVM Properties'] | |||
} | |||
...sysInfoData['System'], | |||
...omit(sysInfoData, [ | |||
'Application Nodes', | |||
'Plugins', | |||
'Search Nodes', | |||
'Settings', | |||
'Statistics', | |||
'System' | |||
]) | |||
}; | |||
} | |||
export function getAppNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { | |||
return sysInfoData['Application Nodes']; | |||
export function getStandaloneMainSections(sysInfoData: SysInfo): SysValueObject { | |||
return { | |||
...sysInfoData['System'], | |||
...omitBy( | |||
sysInfoData, | |||
(value, key) => | |||
value == null || | |||
['Plugins', 'Settings', 'Statistics', 'System'].includes(key) || | |||
key.startsWith('Compute Engine') || | |||
key.startsWith('Search') || | |||
key.startsWith('Web') | |||
) | |||
}; | |||
} | |||
export function getSearchNodes(sysInfoData: ClusterSysInfo): NodeInfo[] { | |||
return sysInfoData['Search Nodes']; | |||
export function getStandaloneSecondarySections(sysInfoData: SysInfo): SysInfoSection { | |||
return { | |||
Web: pickBy(sysInfoData, (_, key) => key.startsWith('Web')), | |||
'Compute Engine': pickBy(sysInfoData, (_, key) => key.startsWith('Compute Engine')), | |||
Search: pickBy(sysInfoData, (_, key) => key.startsWith('Search')) | |||
}; | |||
} | |||
export function groupSections(sysInfoData: SysValueObject) { | |||
@@ -147,18 +155,12 @@ export function groupSections(sysInfoData: SysValueObject) { | |||
return { mainSection, sections }; | |||
} | |||
export function isCluster(sysInfoData?: SysInfo): boolean { | |||
return sysInfoData != undefined && sysInfoData['Cluster'] === true; | |||
} | |||
export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ | |||
expandedCards: parseAsArray(urlQuery.expand, parseAsString) | |||
})); | |||
export const parseQuery = memoize((urlQuery: RawQuery): Query => { | |||
return { | |||
expandedCards: parseAsArray(urlQuery.expand, parseAsString) | |||
}; | |||
}); | |||
export const serializeQuery = memoize((query: Query): RawQuery => { | |||
return cleanQuery({ | |||
export const serializeQuery = memoize((query: Query): RawQuery => | |||
cleanQuery({ | |||
expand: serializeStringArray(query.expandedCards) | |||
}); | |||
}); | |||
}) | |||
); |
@@ -25,7 +25,7 @@ | |||
background-color: #d4333f; | |||
} | |||
.sstatus-indicator.yellow { | |||
.status-indicator.yellow { | |||
background-color: #eabe06; | |||
} | |||
@@ -174,6 +174,10 @@ public class Navigation { | |||
return open("/settings/server_id", ServerIdPage.class); | |||
} | |||
public SystemInfoPage openSystemInfo() { | |||
return open("/admin/system", SystemInfoPage.class); | |||
} | |||
public NotificationsPage openNotifications() { | |||
return open("/account/notifications", NotificationsPage.class); | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* 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.sonarqube.pageobjects; | |||
import com.codeborne.selenide.CollectionCondition; | |||
import com.codeborne.selenide.SelenideElement; | |||
import static com.codeborne.selenide.Condition.exist; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static com.codeborne.selenide.Selenide.$$; | |||
public class SystemInfoPage { | |||
public SystemInfoPage() { | |||
$(".page-title").should(exist).shouldHave(text("System Info")); | |||
} | |||
public SystemInfoPage shouldHaveCards(String... titles) { | |||
$$(".system-info-health-card").shouldHave(CollectionCondition.texts(titles)); | |||
return this; | |||
} | |||
public SystemInfoPageItem getCardItem(String card) { | |||
SelenideElement cardTitle = $$(".system-info-health-card-title").find(text(card)).should(exist); | |||
return new SystemInfoPageItem(cardTitle.parent().parent()); | |||
} | |||
} |
@@ -0,0 +1,87 @@ | |||
/* | |||
* 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.sonarqube.pageobjects; | |||
import com.codeborne.selenide.SelenideElement; | |||
import static com.codeborne.selenide.Condition.exist; | |||
import static com.codeborne.selenide.Condition.text; | |||
public class SystemInfoPageItem { | |||
private final SelenideElement elt; | |||
public SystemInfoPageItem(SelenideElement elt) { | |||
this.elt = elt; | |||
} | |||
public SystemInfoPageItem shouldHaveHealth() { | |||
elt.$(".system-info-health-info .status-indicator").should(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldHaveSection(String section) { | |||
ensureOpen(); | |||
elt.$$("h4").findBy(text(section)).should(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldNotHaveSection(String section) { | |||
ensureOpen(); | |||
elt.$$("h4").findBy(text(section)).shouldNot(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldHaveMainSection() { | |||
ensureOpen(); | |||
elt.$$(".system-info-section").get(0).find("h4").shouldNot(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldHaveField(String field) { | |||
ensureOpen(); | |||
elt.$$(".system-info-section-item-name").findBy(text(field)).should(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldNotHaveField(String field) { | |||
ensureOpen(); | |||
elt.$$(".system-info-section-item-name").findBy(text(field)).shouldNot(exist); | |||
return this; | |||
} | |||
public SystemInfoPageItem shouldHaveFieldWithValue(String field, String value) { | |||
ensureOpen(); | |||
SelenideElement fieldElem = elt.$$(".system-info-section-item-name").findBy(text(field)).should(exist); | |||
fieldElem.parent().parent().$$("td").shouldHaveSize(2).get(1).shouldHave(text(value)); | |||
return this; | |||
} | |||
public SystemInfoPageItem ensureOpen() { | |||
if(!isOpen()) { | |||
elt.click(); | |||
elt.$(".boxed-group-inner").should(exist); | |||
} | |||
return this; | |||
} | |||
private boolean isOpen() { | |||
return elt.$(".boxed-group-inner").exists(); | |||
} | |||
} |
@@ -82,7 +82,7 @@ public class ClusterTest { | |||
app.waitForHealthGreen(); | |||
System.out.println("-----------------------------------------------------------------------"); | |||
String json = app.wsClient().wsConnector().call(new GetRequest("api/system/cluster_info")).content(); | |||
String json = app.wsClient().wsConnector().call(new GetRequest("api/system/info")).content(); | |||
System.out.println(json); | |||
System.out.println("-----------------------------------------------------------------------"); | |||
} |
@@ -27,6 +27,7 @@ import org.apache.commons.io.FileUtils; | |||
import org.junit.ClassRule; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonarqube.pageobjects.SystemInfoPage; | |||
import org.sonarqube.tests.Category4Suite; | |||
import org.sonarqube.tests.Tester; | |||
import org.sonarqube.ws.client.GetRequest; | |||
@@ -48,7 +49,35 @@ public class SystemInfoTest { | |||
@Test | |||
public void test_system_info_page() { | |||
tester.users().generateAdministrator(u -> u.setLogin(ADMIN_USER_LOGIN).setPassword(ADMIN_USER_LOGIN)); | |||
tester.runHtmlTests("/serverSystem/ServerSystemTest/system_info.html"); | |||
SystemInfoPage page = tester.openBrowser().logIn().submitCredentials(ADMIN_USER_LOGIN).openSystemInfo(); | |||
page.shouldHaveCards("System", "Web", "Compute Engine", "Search"); | |||
page.getCardItem("System") | |||
.shouldHaveHealth() | |||
.shouldHaveMainSection() | |||
.shouldHaveSection("Database") | |||
.shouldNotHaveSection("Settings") | |||
.shouldNotHaveSection("Plugins") | |||
.shouldNotHaveSection("Statistics") | |||
.shouldHaveField("Official Distribution") | |||
.shouldHaveField("Version") | |||
.shouldHaveField("Logs Level") | |||
.shouldHaveField("High Availability") | |||
.shouldNotHaveField("Health") | |||
.shouldNotHaveField("Health Causes"); | |||
page.getCardItem("Web") | |||
.shouldHaveSection("Web JVM Properties") | |||
.shouldHaveSection("Web JVM State"); | |||
page.getCardItem("Compute Engine") | |||
.shouldHaveSection("Compute Engine Database Connection") | |||
.shouldHaveSection("Compute Engine JVM State") | |||
.shouldHaveSection("Compute Engine Tasks"); | |||
page.getCardItem("Search") | |||
.shouldHaveSection("Search State") | |||
.shouldHaveSection("Search Statistics"); | |||
} | |||
@Test | |||
@@ -63,9 +92,9 @@ public class SystemInfoTest { | |||
// SONAR-7436 monitor ES and CE | |||
assertThat((Map)json.get("Compute Engine Database Connection")).isNotEmpty(); | |||
assertThat((Map)json.get("Compute Engine State")).isNotEmpty(); | |||
assertThat((Map)json.get("Compute Engine JVM State")).isNotEmpty(); | |||
assertThat((Map)json.get("Compute Engine Tasks")).isNotEmpty(); | |||
Map<String,Object> esJson = (Map) json.get("Elasticsearch"); | |||
Map<String,Object> esJson = (Map) json.get("Search State"); | |||
assertThat(esJson.get("State")).isEqualTo("GREEN"); | |||
// SONAR-7271 get settings |
@@ -36,6 +36,8 @@ import org.sonar.wsclient.base.HttpException; | |||
import org.sonar.wsclient.connectors.HttpClient4Connector; | |||
import org.sonar.wsclient.services.AuthenticationQuery; | |||
import org.sonar.wsclient.user.UserParameters; | |||
import org.sonarqube.pageobjects.SystemInfoPage; | |||
import org.sonarqube.tests.Tester; | |||
import org.sonarqube.ws.client.GetRequest; | |||
import org.sonarqube.ws.client.WsResponse; | |||
import org.sonarqube.ws.client.user.CreateRequest; | |||
@@ -81,6 +83,9 @@ public class RealmAuthenticationTest { | |||
@Rule | |||
public UserRule userRule = UserRule.from(orchestrator); | |||
@Rule | |||
public Tester tester = new Tester(orchestrator).disableOrganizations(); | |||
@Before | |||
@After | |||
public void resetData() throws Exception { | |||
@@ -121,7 +126,8 @@ public class RealmAuthenticationTest { | |||
runSelenese(orchestrator, "/user/ExternalAuthenticationTest/external-user-details.html"); | |||
// SONAR-4462 | |||
runSelenese(orchestrator, "/user/ExternalAuthenticationTest/system-info.html"); | |||
SystemInfoPage page = tester.openBrowser().logIn().submitCredentials(ADMIN_USER_LOGIN).openSystemInfo(); | |||
page.getCardItem("System").shouldHaveFieldWithValue("External User Authentication", "FakeRealm"); | |||
} | |||
/** |
@@ -1,80 +0,0 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |||
<head profile="http://selenium-ide.openqa.org/profiles/test-case"> | |||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> | |||
<title>system_info</title> | |||
</head> | |||
<body> | |||
<table cellpadding="1" cellspacing="1" border="1"> | |||
<thead> | |||
<tr> | |||
<td rowspan="1" colspan="3">system_info</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr> | |||
<td>open</td> | |||
<td>/sessions/new</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>login</td> | |||
<td>admin-user</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>password</td> | |||
<td>admin-user</td> | |||
</tr> | |||
<tr> | |||
<td>clickAndWait</td> | |||
<td>commit</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=.js-user-authenticated</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/system/index</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*Official Distribution*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*Database Version*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*Pool Active Connections*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*Start Time*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*Processors*</td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*java.class.path*java.specification.version*</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</body> | |||
</html> |
@@ -1,44 +0,0 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Strict/EN" "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |||
<html xmlns="http:/www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |||
<head profile="http:/selenium-ide.openqa.org/profiles/test-case"> | |||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> | |||
<title>external_user_details</title> | |||
</head> | |||
<body> | |||
<table cellpadding="1" cellspacing="1" border="1"> | |||
<thead> | |||
<tr> | |||
<td rowspan="1" colspan="3">external_user_details</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr> | |||
<td>open</td> | |||
<td>/system</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>login</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>type</td> | |||
<td>password</td> | |||
<td>admin</td> | |||
</tr> | |||
<tr> | |||
<td>clickAndWait</td> | |||
<td>commit</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForText</td> | |||
<td>id=content</td> | |||
<td>*External User Authentication*FakeRealm*</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</body> | |||
</html> |