This is an automated email from the ASF dual-hosted git repository.
adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 2c30d6f2924 HDDS-16138. Add compatibility coverage for SCM Ratis role
output over IPv6 (#11049)
2c30d6f2924 is described below
commit 2c30d6f2924d004fe78dbb2ccf60333ddfd8b162
Author: rjgoyln <[email protected]>
AuthorDate: Sat Sep 19 03:26:23 2026 +0800
HDDS-16138. Add compatibility coverage for SCM Ratis role output over IPv6
(#11049)
---
.../hadoop/hdds/scm/ha/TestSCMRatisServerImpl.java | 93 ++++++++++++---
.../TestStorageContainerManagerRatisRoles.java | 126 +++++++++++++++++++++
.../hdds/scm/cli/SafeModeCheckSubcommand.java | 10 +-
.../hdds/scm/cli/TestSafeModeCheckSubcommand.java | 123 ++++++++++++++++++++
.../ozone/scm/TestGetScmRatisRolesSubcommand.java | 85 +++++++++++++-
.../TestStorageContainerServiceProviderImpl.java | 53 +++++++++
6 files changed, 465 insertions(+), 25 deletions(-)
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestSCMRatisServerImpl.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestSCMRatisServerImpl.java
index 56b99fb8f65..8ff9a2fb00a 100644
---
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestSCMRatisServerImpl.java
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/ha/TestSCMRatisServerImpl.java
@@ -17,9 +17,9 @@
package org.apache.hadoop.hdds.scm.ha;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -28,9 +28,10 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import java.util.Arrays;
import java.util.List;
import java.util.UUID;
-import org.apache.hadoop.hdds.HddsUtils;
+import java.util.stream.Stream;
import org.apache.hadoop.hdds.conf.ConfigurationSource;
import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
import org.apache.hadoop.hdds.security.SecurityConfig;
@@ -41,6 +42,9 @@
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.server.RaftServer;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
@@ -108,8 +112,68 @@ public void testGetLeaderId() throws Exception {
}
}
+ static Stream<Arguments> peerAddressEncodings() {
+ return Stream.of(
+ Arguments.of("10.0.0.1:9894", "10.0.0.1:9894:LEADER:peer1:10.0.0.1"),
+ Arguments.of("[2001:db8::1]:9894",
"[2001:db8::1]:9894:LEADER:peer1:[2001:db8::1]"));
+ }
+
+ /**
+ * The encoding is a wire format shared with every consumer of
+ * {@code ozone admin scm roles}, so it is asserted verbatim rather than
+ * round-tripped through the parser that reads it back.
+ */
+ @ParameterizedTest
+ @MethodSource("peerAddressEncodings")
+ public void testGetRatisRolesEncoding(String peerAddress, String
expectedRole) throws Exception {
+ try (
+ MockedConstruction<SecurityConfig> mockedSecurityConfigConstruction =
mockConstruction(SecurityConfig.class);
+ MockedStatic<RaftServer> staticMockedRaftServer =
mockStatic(RaftServer.class);
+ MockedStatic<RatisUtil> staticMockedRatisUtil =
mockStatic(RatisUtil.class);
+ ) {
+ ConfigurationSource conf = mock(ConfigurationSource.class);
+ StorageContainerManager scm = mock(StorageContainerManager.class);
+ when(scm.getClusterId()).thenReturn("CID-" + UUID.randomUUID());
+ SCMHADBTransactionBuffer dbTransactionBuffer =
mock(SCMHADBTransactionBuffer.class);
+
+ RaftServer.Builder raftServerBuilder = mock(RaftServer.Builder.class);
+ when(raftServerBuilder.setServerId(any())).thenReturn(raftServerBuilder);
+
when(raftServerBuilder.setProperties(any())).thenReturn(raftServerBuilder);
+
when(raftServerBuilder.setStateMachineRegistry(any())).thenReturn(raftServerBuilder);
+ when(raftServerBuilder.setOption(any())).thenReturn(raftServerBuilder);
+ when(raftServerBuilder.setGroup(any())).thenReturn(raftServerBuilder);
+
when(raftServerBuilder.setParameters(any())).thenReturn(raftServerBuilder);
+
+ RaftServer raftServer = mock(RaftServer.class);
+ RaftServer.Division division = mock(RaftServer.Division.class);
+ when(raftServer.getDivision(any())).thenReturn(division);
+ when(raftServerBuilder.build()).thenReturn(raftServer);
+
staticMockedRaftServer.when(RaftServer::newBuilder).thenReturn(raftServerBuilder);
+
+ RaftProperties raftProperties = mock(RaftProperties.class);
+ staticMockedRatisUtil.when(() ->
RatisUtil.newRaftProperties(conf)).thenReturn(raftProperties);
+
+ SecurityConfig sc = new SecurityConfig(conf);
+ when(sc.isSecurityEnabled()).thenReturn(false);
+
+ SCMRatisServerImpl scmRatisServer = spy(new SCMRatisServerImpl(conf,
scm, dbTransactionBuffer));
+
+ RaftPeer peer = RaftPeer.newBuilder()
+ .setId(RaftPeerId.valueOf("peer1"))
+ .setAddress(peerAddress)
+ .build();
+
+
when(division.getGroup()).thenReturn(RaftGroup.valueOf(RaftGroupId.randomId(),
peer));
+ doReturn(peer).when(scmRatisServer).getLeader();
+
+ List<String> roles = scmRatisServer.getRatisRoles();
+
+ assertEquals(Arrays.asList(expectedRole), roles);
+ }
+ }
+
@Test
- public void testGetRatisRolesWithIPv6() throws Exception {
+ public void testGetRatisRolesKeepsHostname() throws Exception {
try (
MockedConstruction<SecurityConfig> mockedSecurityConfigConstruction =
mockConstruction(SecurityConfig.class);
MockedStatic<RaftServer> staticMockedRaftServer =
mockStatic(RaftServer.class);
@@ -142,26 +206,21 @@ public void testGetRatisRolesWithIPv6() throws Exception {
SCMRatisServerImpl scmRatisServer = spy(new SCMRatisServerImpl(conf,
scm, dbTransactionBuffer));
- // IPv6 peer address in the bracketed format that getRatisHostPortStr()
produces
- RaftPeer ipv6Peer = RaftPeer.newBuilder()
+ RaftPeer peer = RaftPeer.newBuilder()
.setId(RaftPeerId.valueOf("peer1"))
- .setAddress("[2001:db8::1]:9894")
+ .setAddress("localhost:9894")
.build();
- RaftGroup raftGroup = RaftGroup.valueOf(RaftGroupId.randomId(),
ipv6Peer);
- when(division.getGroup()).thenReturn(raftGroup);
- doReturn(ipv6Peer).when(scmRatisServer).getLeader();
+
when(division.getGroup()).thenReturn(RaftGroup.valueOf(RaftGroupId.randomId(),
peer));
+ doReturn(peer).when(scmRatisServer).getLeader();
List<String> roles = scmRatisServer.getRatisRoles();
- assertEquals(1, roles.size());
- String roleString = roles.get(0);
- String[] parsed = HddsUtils.parseRatisRoleString(roleString);
- assertEquals("2001:db8::1", parsed[0]);
- assertEquals("9894", parsed[1]);
- assertEquals("LEADER", parsed[2]);
- assertEquals("peer1", parsed[3]);
- assertTrue(!parsed[4].isEmpty(), "hostIP should be resolved");
+ // Which address a name resolves to is the resolver's business, so only
+ // the fields the encoding itself owns are pinned here.
+ assertEquals(1, roles.size());
+ assertThat(roles.get(0)).startsWith("localhost:9894:LEADER:peer1:");
+ assertThat(roles.get(0)).doesNotEndWith(":");
}
}
diff --git
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestStorageContainerManagerRatisRoles.java
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestStorageContainerManagerRatisRoles.java
new file mode 100644
index 00000000000..e3c9e35e121
--- /dev/null
+++
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/server/TestStorageContainerManagerRatisRoles.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.server;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hdds.scm.ha.SCMHAManager;
+import org.apache.hadoop.hdds.scm.ha.SCMRatisServer;
+import org.apache.ratis.protocol.RaftPeerId;
+import org.apache.ratis.server.DivisionInfo;
+import org.apache.ratis.server.RaftServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests the SCM roles exposed over JMX, which the SCM web UI renders. The
+ * columns are reordered relative to the encoded role string, so the mapping
+ * is asserted field by field.
+ */
+public class TestStorageContainerManagerRatisRoles {
+
+ private static final String LEADER_ID =
"e428ca07-b2a3-4756-bf9b-a4abb033c7d1";
+ private static final String FOLLOWER_ID =
"61b1c8e5-da40-4567-8a17-96a0234ba14e";
+
+ private static StorageContainerManager scmWith(SCMRatisServer server) {
+ SCMHAManager haManager = mock(SCMHAManager.class);
+ when(haManager.getRatisServer()).thenReturn(server);
+
+ StorageContainerManager scm = mock(StorageContainerManager.class);
+ when(scm.getScmHAManager()).thenReturn(haManager);
+ doCallRealMethod().when(scm).getScmRatisRoles();
+ return scm;
+ }
+
+ private static SCMRatisServer ratisServer(RaftPeerId leaderId, List<String>
roles) {
+ DivisionInfo info = mock(DivisionInfo.class);
+ when(info.getLeaderId()).thenReturn(leaderId);
+
+ RaftServer.Division division = mock(RaftServer.Division.class);
+ when(division.getInfo()).thenReturn(info);
+
+ SCMRatisServer server = mock(SCMRatisServer.class);
+ when(server.getDivision()).thenReturn(division);
+ when(server.isStopped()).thenReturn(false);
+ when(server.getRatisRoles()).thenReturn(roles);
+ return server;
+ }
+
+ private static SCMRatisServer healthyServer(List<String> roles) {
+ return ratisServer(RaftPeerId.valueOf("scm1"), roles);
+ }
+
+ @Test
+ public void testGetScmRatisRolesIPv4() {
+ StorageContainerManager scm = scmWith(healthyServer(Arrays.asList(
+ "scm1.example.com:9894:LEADER:" + LEADER_ID + ":10.0.0.1",
+ "scm2.example.com:9894:FOLLOWER:" + FOLLOWER_ID + ":10.0.0.2")));
+
+ assertEquals(
+ Arrays.asList(
+ Arrays.asList("scm1.example.com", LEADER_ID, "9894", "LEADER"),
+ Arrays.asList("scm2.example.com", FOLLOWER_ID, "9894",
"FOLLOWER")),
+ scm.getScmRatisRoles());
+ }
+
+ @Test
+ public void testGetScmRatisRolesIPv6() {
+ StorageContainerManager scm = scmWith(healthyServer(Arrays.asList(
+ "[2001:db8::1]:9894:LEADER:" + LEADER_ID + ":[2001:db8:0:0:0:0:0:1]",
+ "[2001:db8::2]:9894:FOLLOWER:" + FOLLOWER_ID +
":[2001:db8:0:0:0:0:0:2]")));
+
+ // The host column is a display value, so the brackets that make the
+ // encoded field unambiguous are stripped back off.
+ assertEquals(
+ Arrays.asList(
+ Arrays.asList("2001:db8::1", LEADER_ID, "9894", "LEADER"),
+ Arrays.asList("2001:db8::2", FOLLOWER_ID, "9894", "FOLLOWER")),
+ scm.getScmRatisRoles());
+ }
+
+ @Test
+ public void testGetScmRatisRolesWithoutLeader() {
+ StorageContainerManager scm = scmWith(ratisServer(null,
Collections.emptyList()));
+
+ assertEquals(Collections.singletonList(Collections.singletonList("No
leader found")),
+ scm.getScmRatisRoles());
+ }
+
+ /**
+ * Verify that the JMX view reports invalid role strings as an error row.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"", "scm1.example.com:9894"})
+ public void testGetScmRatisRolesWithUnparseableRole(String role) {
+ StorageContainerManager scm =
scmWith(healthyServer(Collections.singletonList(role)));
+
+ List<List<String>> roles = scm.getScmRatisRoles();
+
+ assertEquals(1, roles.size());
+ assertEquals(1, roles.get(0).size());
+ assertThat(roles.get(0).get(0)).startsWith("Exception Occurred, ");
+ }
+}
diff --git
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/SafeModeCheckSubcommand.java
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/SafeModeCheckSubcommand.java
index d9064640105..12d58d66237 100644
---
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/SafeModeCheckSubcommand.java
+++
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/SafeModeCheckSubcommand.java
@@ -17,6 +17,7 @@
package org.apache.hadoop.hdds.scm.cli;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
@@ -84,7 +85,7 @@ private void executeForSingleNode(ScmClient scmClient,
ScmNodeTarget targetScmNo
SCMNodeInfo targetNode;
if (serviceId != null) {
// HA mode: find leader
- targetNode = findLeaderNode(scmClient);
+ targetNode = findLeaderNode(scmClient, nodes);
if (targetNode == null) {
throw new IOException("Could not determine leader node");
}
@@ -99,9 +100,12 @@ private void executeForSingleNode(ScmClient scmClient,
ScmNodeTarget targetScmNo
/**
* Find the leader node from SCM roles.
* @param scmClient the SCM client
+ * @param nodes the SCM nodes configured for the service
* @return the leader SCMNodeInfo
*/
- private SCMNodeInfo findLeaderNode(ScmClient scmClient) throws IOException {
+ @VisibleForTesting
+ static SCMNodeInfo findLeaderNode(ScmClient scmClient, List<SCMNodeInfo>
nodes)
+ throws IOException {
try {
List<String> roles = scmClient.getScmRoles();
for (String role : roles) {
@@ -188,7 +192,7 @@ private void queryNode(ScmClient scmClient, ScmNodeTarget
targetScmNode, SCMNode
* Inputs may be bare hosts or host:port strings. Handles IPv6 equivalence
* (e.g. 2001:db8::1 vs 2001:db8:0:0:0:0:0:1) by resolving to InetAddress.
*/
- private boolean matchesAddress(String address1, String address2) {
+ private static boolean matchesAddress(String address1, String address2) {
if (address1.equalsIgnoreCase(address2)) {
return true;
}
diff --git
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestSafeModeCheckSubcommand.java
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestSafeModeCheckSubcommand.java
new file mode 100644
index 00000000000..a2af95748f9
--- /dev/null
+++
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/TestSafeModeCheckSubcommand.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.cli;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hadoop.hdds.scm.client.ScmClient;
+import org.apache.hadoop.hdds.scm.ha.SCMNodeInfo;
+import org.apache.hadoop.hdds.scm.net.HostAndPort;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests leader detection in `ozone admin safemode status`, which picks its
+ * target node by parsing the Ratis role strings returned by SCM.
+ */
+public class TestSafeModeCheckSubcommand {
+
+ private static final String LEADER_ID =
"e428ca07-b2a3-4756-bf9b-a4abb033c7d1";
+ private static final String FOLLOWER_ID =
"61b1c8e5-da40-4567-8a17-96a0234ba14e";
+
+ private static SCMNodeInfo node(String nodeId, String host) {
+ HostAndPort address = new HostAndPort(host, 9860);
+ return new SCMNodeInfo("scmservice", nodeId, address, address, address,
address);
+ }
+
+ private static SCMNodeInfo findLeader(List<String> roles, List<SCMNodeInfo>
nodes)
+ throws IOException {
+ ScmClient scmClient = mock(ScmClient.class);
+ when(scmClient.getScmRoles()).thenReturn(roles);
+
+ return SafeModeCheckSubcommand.findLeaderNode(scmClient, nodes);
+ }
+
+ @Test
+ public void testFindLeaderNodeIPv4() throws Exception {
+ SCMNodeInfo scm1 = node("scm1", "10.0.0.1");
+ SCMNodeInfo scm2 = node("scm2", "10.0.0.2");
+
+ SCMNodeInfo leader = findLeader(
+ Arrays.asList(
+ "scm2.example.com:9894:FOLLOWER:" + FOLLOWER_ID + ":10.0.0.2",
+ "scm1.example.com:9894:LEADER:" + LEADER_ID + ":10.0.0.1"),
+ Arrays.asList(scm1, scm2));
+
+ // The configured nodes carry addresses, not DNS names, so the match comes
+ // from the trailing resolved-address field rather than the host field.
+ assertEquals(scm1, leader);
+ }
+
+ @Test
+ public void testFindLeaderNodeIPv6() throws Exception {
+ SCMNodeInfo scm1 = node("scm1", "2001:db8::1");
+ SCMNodeInfo scm2 = node("scm2", "2001:db8::2");
+
+ SCMNodeInfo leader = findLeader(
+ Arrays.asList(
+ "[2001:db8::2]:9894:FOLLOWER:" + FOLLOWER_ID +
":[2001:db8:0:0:0:0:0:2]",
+ "[2001:db8::1]:9894:LEADER:" + LEADER_ID +
":[2001:db8:0:0:0:0:0:1]"),
+ Arrays.asList(scm1, scm2));
+
+ assertEquals(scm1, leader);
+ }
+
+ @Test
+ public void testFindLeaderNodeMatchesEquivalentIPv6Forms() throws Exception {
+ // SCM reports the compressed form while the node is configured with the
+ // expanded one; the two denote the same address and must still match.
+ SCMNodeInfo scm1 = node("scm1", "2001:db8:0:0:0:0:0:1");
+
+ SCMNodeInfo leader = findLeader(
+ Arrays.asList("[2001:db8::1]:9894:LEADER:" + LEADER_ID +
":[2001:db8::1]"),
+ Arrays.asList(scm1));
+
+ assertEquals(scm1, leader);
+ }
+
+ @Test
+ public void testFindLeaderNodeWithoutLeader() throws Exception {
+ SCMNodeInfo leader = findLeader(
+ Arrays.asList("[2001:db8::1]:9894:FOLLOWER:" + FOLLOWER_ID +
":[2001:db8::1]"),
+ Arrays.asList(node("scm1", "2001:db8::1")));
+
+ assertNull(leader);
+ }
+
+ /**
+ * Verify that leader detection skips invalid role strings and checks later
entries.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"", "Exception Occurred, No leader found"})
+ public void testFindLeaderNodeSkipsUnparseableRole(String unparseable)
throws Exception {
+ SCMNodeInfo scm1 = node("scm1", "2001:db8::1");
+
+ SCMNodeInfo leader = findLeader(
+ Arrays.asList(unparseable, "[2001:db8::1]:9894:LEADER:" + LEADER_ID +
":[2001:db8::1]"),
+ Arrays.asList(scm1));
+
+ assertEquals(scm1, leader);
+ }
+}
diff --git
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/ozone/scm/TestGetScmRatisRolesSubcommand.java
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/ozone/scm/TestGetScmRatisRolesSubcommand.java
index 52c4a4cda2e..948d7ad0f3c 100644
---
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/ozone/scm/TestGetScmRatisRolesSubcommand.java
+++
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/ozone/scm/TestGetScmRatisRolesSubcommand.java
@@ -118,11 +118,86 @@ public void testGetScmHARatisRolesIPv6() throws Exception
{
try (GenericTestUtils.SystemOutCapturer capture =
new GenericTestUtils.SystemOutCapturer()) {
cmd.execute(client);
- assertThat(capture.getOutput()).contains("2001:db8::1");
- assertThat(capture.getOutput()).contains("9894");
- assertThat(capture.getOutput()).contains("LEADER");
-
assertThat(capture.getOutput()).contains("e428ca07-b2a3-4756-bf9b-a4abb033c7d1");
- assertThat(capture.getOutput()).contains("FOLLOWER");
+ // Column widths depend on the rendered content, but the column order and
+ // the value in each one must not shift.
+ assertThat(capture.getOutput()).containsPattern(
+ "\\|\\s+2001:db8::1\\s+\\|\\s+9894\\s+\\|\\s+LEADER\\s+\\|"
+ +
"\\s+e428ca07-b2a3-4756-bf9b-a4abb033c7d1\\s+\\|\\s+2001:db8:0:0:0:0:0:1\\s+\\|");
+ assertThat(capture.getOutput()).containsPattern(
+ "\\|\\s+2001:db8::2\\s+\\|\\s+9894\\s+\\|\\s+FOLLOWER\\s+\\|"
+ +
"\\s+61b1c8e5-da40-4567-8a17-96a0234ba14e\\s+\\|\\s+2001:db8:0:0:0:0:0:2\\s+\\|");
+ }
+ }
+
+ @Test
+ public void testGetScmRatisRolesJson() throws Exception {
+ GetScmRatisRolesSubcommand cmd = new GetScmRatisRolesSubcommand();
+ ScmClient client = mock(ScmClient.class);
+ CommandLine c = new CommandLine(cmd);
+ c.parseArgs("--json");
+
+ List<String> result = new ArrayList<>();
+
result.add("scm1.example.com:9894:LEADER:e428ca07-b2a3-4756-bf9b-a4abb033c7d1:10.0.0.1");
+
+ when(client.getScmRoles()).thenAnswer(invocation -> result);
+
+ try (GenericTestUtils.SystemOutCapturer capture =
+ new GenericTestUtils.SystemOutCapturer()) {
+ cmd.execute(client);
+ assertThat(JsonUtils.readTree(capture.getOutput())).isEqualTo(
+ JsonUtils.readTree("{\"scm1.example.com\":{"
+ + "\"address\":\"scm1.example.com:9894\","
+ + "\"raftPeerRole\":\"LEADER\","
+ + "\"ID\":\"e428ca07-b2a3-4756-bf9b-a4abb033c7d1\","
+ + "\"InetAddress\":\"10.0.0.1\"}}"));
+ }
+ }
+
+ @Test
+ public void testGetScmHARatisRolesIPv6Json() throws Exception {
+ GetScmRatisRolesSubcommand cmd = new GetScmRatisRolesSubcommand();
+ ScmClient client = mock(ScmClient.class);
+ CommandLine c = new CommandLine(cmd);
+ c.parseArgs("--json");
+
+ List<String> result = new ArrayList<>();
+
result.add("[2001:db8::1]:9894:LEADER:e428ca07-b2a3-4756-bf9b-a4abb033c7d1:[2001:db8:0:0:0:0:0:1]");
+
+ when(client.getScmRoles()).thenAnswer(invocation -> result);
+
+ try (GenericTestUtils.SystemOutCapturer capture =
+ new GenericTestUtils.SystemOutCapturer()) {
+ cmd.execute(client);
+ // The address is a connectable target so it keeps its brackets; the
+ // resolved literal in InetAddress is a bare address and stays
unbracketed.
+ assertThat(JsonUtils.readTree(capture.getOutput())).isEqualTo(
+ JsonUtils.readTree("{\"2001:db8::1\":{"
+ + "\"address\":\"[2001:db8::1]:9894\","
+ + "\"raftPeerRole\":\"LEADER\","
+ + "\"ID\":\"e428ca07-b2a3-4756-bf9b-a4abb033c7d1\","
+ + "\"InetAddress\":\"2001:db8:0:0:0:0:0:1\"}}"));
+ }
+ }
+
+ @Test
+ public void testGetScmRatisRolesDefaultOutputIsVerbatim() throws Exception {
+ GetScmRatisRolesSubcommand cmd = new GetScmRatisRolesSubcommand();
+ ScmClient client = mock(ScmClient.class);
+ CommandLine c = new CommandLine(cmd);
+ c.parseArgs();
+
+ List<String> result = new ArrayList<>();
+
result.add("scm1.example.com:9894:LEADER:e428ca07-b2a3-4756-bf9b-a4abb033c7d1:10.0.0.1");
+
result.add("[2001:db8::1]:9894:FOLLOWER:61b1c8e5-da40-4567-8a17-96a0234ba14e:[2001:db8:0:0:0:0:0:1]");
+
+ when(client.getScmRoles()).thenAnswer(invocation -> result);
+
+ try (GenericTestUtils.SystemOutCapturer capture =
+ new GenericTestUtils.SystemOutCapturer()) {
+ cmd.execute(client);
+ for (String role : result) {
+ assertThat(capture.getOutput()).contains(role);
+ }
}
}
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestStorageContainerServiceProviderImpl.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestStorageContainerServiceProviderImpl.java
index 6bf88bc216c..83de7a0917e 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestStorageContainerServiceProviderImpl.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestStorageContainerServiceProviderImpl.java
@@ -17,9 +17,13 @@
package org.apache.hadoop.ozone.recon.spi.impl;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -29,17 +33,26 @@
import com.google.inject.Injector;
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.ScmInfo;
+import org.apache.hadoop.hdds.scm.ha.InterSCMGrpcClient;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.scm.pipeline.PipelineID;
import org.apache.hadoop.hdds.scm.protocol.StorageContainerLocationProtocol;
+import org.apache.hadoop.hdds.utils.HddsServerUtil;
import org.apache.hadoop.ozone.recon.ReconUtils;
+import org.apache.hadoop.ozone.recon.security.ReconCertificateClient;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
/**
* Class to test StorageContainerServiceProviderImpl APIs.
@@ -98,4 +111,44 @@ public void testGetPipeline() throws IOException {
verify(scmClient, times(1))
.getPipeline(pipelineID);
}
+
+ /**
+ * Recon downloads the SCM snapshot from whichever peer reports itself as
+ * LEADER, using the resolved address carried in the encoded role string.
+ */
+ @Test
+ public void testGetSCMDBSnapshotConnectsToLeader() throws Exception {
+ StorageContainerServiceProvider scmProvider =
+ injector.getInstance(StorageContainerServiceProvider.class);
+ StorageContainerLocationProtocol scmClient =
+ injector.getInstance(StorageContainerLocationProtocol.class);
+
+ when(scmClient.getScmInfo()).thenReturn(new ScmInfo.Builder()
+ .setClusterId("CID-6a1b5b1e-3f5a-4b5c-8f6d-2a1c3e4f5a6b")
+ .setScmId("scm1")
+ .setPeerRoles(Arrays.asList(
+
"[2001:db8::2]:9894:FOLLOWER:61b1c8e5-da40-4567-8a17-96a0234ba14e:[2001:db8:0:0:0:0:0:2]",
+
"[2001:db8::1]:9894:LEADER:e428ca07-b2a3-4756-bf9b-a4abb033c7d1:[2001:db8:0:0:0:0:0:1]"))
+ .build());
+
+ AtomicReference<String> downloadHost = new AtomicReference<>();
+ try (MockedStatic<HddsServerUtil> stubbedServerUtil =
mockStatic(HddsServerUtil.class);
+ MockedConstruction<ReconCertificateClient> stubbedCertClient =
+ mockConstruction(ReconCertificateClient.class);
+ MockedConstruction<InterSCMGrpcClient> downloaders =
+ mockConstruction(InterSCMGrpcClient.class, (downloader, context)
-> {
+ downloadHost.set((String) context.arguments().get(0));
+ when(downloader.download(any()))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ })) {
+ scmProvider.getSCMDBSnapshot();
+
+ assertEquals(1, downloaders.constructed().size());
+ }
+
+ // The follower is listed first, so this also pins that the LEADER entry is
+ // the one selected. The resolved literal is handed over verbatim,
unbracketed.
+ assertEquals("2001:db8:0:0:0:0:0:1", downloadHost.get());
+ }
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]