HDDS-15774. Make SCM Ratis roles string parsing IPv6-safe#10831
HDDS-15774. Make SCM Ratis roles string parsing IPv6-safe#10831AdyChechani wants to merge 1 commit into
Conversation
chihsuan
left a comment
There was a problem hiding this comment.
Thanks for the fix! @AdyChechani
I think structured representation would be the proper long-term fix (mentioned in Jira), but it requires broader API changes, so the shared-parser approach looks like a good scoped fix for now.
I found a few issues that might be worth addressing and left inline comments.
|
|
||
| // Field 4: id (uuid or peer id, no colons) | ||
| int sep3 = roleString.lastIndexOf(':', idx - 1); | ||
| String id = roleString.substring(sep3 + 1, idx); |
There was a problem hiding this comment.
Could we validate the input before calling substring()? Inputs such as "" or "host:9894" currently produce StringIndexOutOfBoundsException but older SCMs can actually emit.
| String leaderIp = parts[4]; | ||
| for (SCMNodeInfo node : nodes) { | ||
| String nodeHost = node.getScmClientAddress().split(":")[0]; | ||
| String nodeHost = HddsUtils.getHostName(node.getScmClientAddress()).orElse(""); |
There was a problem hiding this comment.
This correctly extracts the IPv6 host, but matchesAddress() still parses its inputs with split(":", 2). Equivalent forms such as 2001:db8::1 and 2001:db8:0:0:0:0:0:1 therefore do not match. Could we make matchesAddress() IPv6-aware as part of this fix?
| roleDetails.put("InetAddress", roles[4]); | ||
| } | ||
| allRoles.put(roles[0], roleDetails); | ||
| String[] fields = HddsUtils.parseRatisRoleString(role); |
There was a problem hiding this comment.
Should we preserve the existing host:port handling here? The previous code explicitly supported this non-Ratis/two-field response, but the new parser requires all five fields and crashes on "host:9894". This looks like an unintended behavior change.
| if (roleItems.length < 2) { | ||
| err.println("Invalid response received for ScmRatisRoles."); | ||
| } | ||
| String[] roleItems = HddsUtils.parseRatisRoleString(role); |
There was a problem hiding this comment.
The table path previously handled short or invalid responses without throwing. Since the new parser requires five fields, should we preserve the existing validation/skip behavior here?
| } | ||
| allRoles.put(roles[0], roleDetails); | ||
| String[] fields = HddsUtils.parseRatisRoleString(role); | ||
| roleDetails.put("address", fields[0] + ":" + fields[1]); |
There was a problem hiding this comment.
For IPv6 this produces an ambiguous address such as 2001:db8::1:9894. Could we use HddsUtils.getHostPortString(...) here so the JSON output remains [2001:db8::1]:9894?
| continue; | ||
| } | ||
| String host = HddsUtils.getHostName(peer.getAddress()).orElse(""); | ||
| int port = HddsUtils.getHostPort(peer.getAddress()).orElse(0); |
There was a problem hiding this comment.
Is a missing Ratis port valid here? Defaulting to 0 makes the response look valid and consumers may display it as the actual port. If the port is required, failing with a clear message may be safer than silently using 0.
| String hostIp = ""; | ||
| if (peerInetAddress != null) { | ||
| String rawIp = peerInetAddress.getHostAddress(); | ||
| hostIp = rawIp.contains(":") ? "[" + rawIp + "]" : rawIp; |
There was a problem hiding this comment.
Small suggestion: since we already have the InetAddress, InetAddresses.toUriString(peerInetAddress) handles this directly—IPv4 stays unchanged and IPv6 is bracketed. That may make the formatting intent a little clearer.
There was a problem hiding this comment.
Let's fix HostAndPort class as well:
diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java
index ... .. ...
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/net/HostAndPort.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.scm.net;
import java.net.InetSocketAddress;
+import org.apache.hadoop.hdds.HddsUtils;
import org.apache.hadoop.net.NetUtils;
public class HostAndPort {
@@ -35,7 +36,7 @@ public class HostAndPort {
public HostAndPort(String host, int port) {
this.host = host;
this.port = port;
- this.hostAndPortString = host + ":" + port;
+ this.hostAndPortString = HddsUtils.getHostPortString(host, port);
this.hash = host.hashCode() ^ Integer.hashCode(port);
// TODO: HDDS-15533 change the address resolution logic and make this.address threadsafe.
this.address = NetUtils.createSocketAddr(hostAndPortString);Otherwise SCMNodeInfo.buildNodeInfo() can still produce invalid unbracketed authority.
Generated-by: Claude Code (Opus 4.6)
What changes were proposed in this pull request?
SCMRatisServerImpl.getRatisRoles()encodes each peer as a colon-delimited stringhost:ratisPort:ROLE:uuid:hostIP. Five consumers split this string on:with hardcoded indices. When the host or hostIP is an IPv6 literal (e.g.2001:db8::1), the extra colons shatter the split,parts[2]is no longerLEADER/FOLLOWER, and downstream code crashes or silently produces wrong results.This PR fixes both the producer and all consumers:
SCMRatisServerImpl.getRatisRoles()):peer.getAddress()throughHddsUtils.getHostName()/getHostPort()/getHostPortString(), which brackets IPv6 hosts (e.g.[2001:db8::1]:9894).peerInetAddress.getHostAddress()when it contains colons.The resulting string for IPv6 is now:
[2001:db8::1]:9894:LEADER:peer1:[2001:db8:0:0:0:0:0:1]Shared parser (
HddsUtils.parseRatisRoleString()):HostAndPortfor the host:port portion. Returns a 5-elementString[]:[host, port, role, id, hostIP].Consumers (all 4 sites):
StorageContainerManager.getScmRatisRoles()— replacedrole.split(":")withHddsUtils.parseRatisRoleString(role).GetScmRatisRolesSubcommand(table and JSON paths) — same replacement.StorageContainerServiceProviderImpl.getSCMDBSnapshot()(Recon) — same.SafeModeCheckSubcommand.findLeaderNode()— same, plus fixed node address comparison to useHddsUtils.getHostName()instead ofsplit(":")[0].No
.protoschema change is needed: the wire field remainsrepeated string peerRolesinGetScmInfoResponseProto. Only the string content changes (IPv6 literals are now bracketed), which is transparent to protobuf.What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-15774
How was this patch tested?
TestHddsUtils#testParseRatisRoleStringIPv4,testParseRatisRoleStringIPv6,testParseRatisRoleStringIPv6Follower, andtestParseRatisRoleStringEmptyHostIpcovering the parser with IPv4, IPv6 (leader/follower), and edge cases.TestSCMRatisServerImpl#testGetRatisRolesWithIPv6, end-to-end test that creates a mock RaftPeer with a bracketed IPv6 address and verifies the produced string parses correctly.TestGetScmRatisRolesSubcommand#testGetScmHARatisRolesIPv6, verifies the CLI--tableoutput renders correctly for IPv6 role strings.testGetScmHARatisRoles(IPv4) continues to pass unchanged.checkstyle.sh, rat.sh, and author.shpass locallymvn clean install -DskipTestssucceeds