-
Notifications
You must be signed in to change notification settings - Fork 621
HDDS-15774. Make SCM Ratis roles string parsing IPv6-safe #10831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -243,6 +243,52 @@ public static String getHostPortString(String host, int port) { | |
| return HostAndPort.fromParts(host, port).toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Parse a Ratis role string produced by | ||
| * {@code SCMRatisServerImpl.getRatisRoles()} into its constituent fields. | ||
| * The format is {@code [host]:port:ROLE:id:hostIP} where host and hostIP | ||
| * may be bracketed IPv6 literals. | ||
| * | ||
| * @param roleString the encoded role string | ||
| * @return a 5-element array: {host, port, role, id, hostIP} | ||
| */ | ||
| public static String[] parseRatisRoleString(String roleString) { | ||
| // Parse from the right: the last field is hostIP (possibly bracketed), | ||
| // then id (uuid, no colons), then role (LEADER/FOLLOWER, no colons), | ||
| // and the remainder is host:port (which may be bracketed IPv6). | ||
| int idx = roleString.length(); | ||
|
|
||
| // Field 5: hostIP — may be bracketed IPv6 like [2001:db8::1] | ||
| String hostIp; | ||
| if (idx > 0 && roleString.charAt(idx - 1) == ']') { | ||
| int bracket = roleString.lastIndexOf('['); | ||
| hostIp = roleString.substring(bracket + 1, idx - 1); | ||
| idx = bracket - 1; // skip the ':' before '[' | ||
| } else { | ||
| int sep = roleString.lastIndexOf(':'); | ||
| hostIp = roleString.substring(sep + 1); | ||
| idx = sep; | ||
| } | ||
|
|
||
| // Field 4: id (uuid or peer id, no colons) | ||
| int sep3 = roleString.lastIndexOf(':', idx - 1); | ||
| String id = roleString.substring(sep3 + 1, idx); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we validate the input before calling |
||
| idx = sep3; | ||
|
|
||
| // Field 3: role (LEADER/FOLLOWER, no colons) | ||
| int sep2 = roleString.lastIndexOf(':', idx - 1); | ||
| String role = roleString.substring(sep2 + 1, idx); | ||
| idx = sep2; | ||
|
|
||
| // Remainder is host:port — use HostAndPort to parse safely | ||
| String hostPort = roleString.substring(0, idx); | ||
| HostAndPort hp = HostAndPort.fromString(hostPort); | ||
| String host = hp.getHost(); | ||
| String port = String.valueOf(hp.getPort()); | ||
|
|
||
| return new String[]{host, port, role, id, hostIp}; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve a number, trying the supplied config keys in order. | ||
| * Each config value may be absent | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -289,13 +289,24 @@ public List<String> getRatisRoles() { | |
| LOG.error("SCM Ratis PeerInetAddress {} is unresolvable", | ||
| peer.getAddress()); | ||
| } | ||
| ratisRoles.add((peer.getAddress() == null ? "" : | ||
| peer.getAddress().concat(peer.equals(leader) ? | ||
| ":".concat(RaftProtos.RaftPeerRole.LEADER.toString()) : | ||
| ":".concat(RaftProtos.RaftPeerRole.FOLLOWER.toString())) | ||
| .concat(":".concat(peer.getId().toString())) | ||
| .concat(":".concat(peerInetAddress == null ? "" : | ||
| peerInetAddress.getHostAddress())))); | ||
| if (peer.getAddress() == null) { | ||
| ratisRoles.add(""); | ||
| continue; | ||
| } | ||
| String host = HddsUtils.getHostName(peer.getAddress()).orElse(""); | ||
| int port = HddsUtils.getHostPort(peer.getAddress()).orElse(0); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 normalizedAddress = HddsUtils.getHostPortString(host, port); | ||
| String role = peer.equals(leader) | ||
| ? RaftProtos.RaftPeerRole.LEADER.toString() | ||
| : RaftProtos.RaftPeerRole.FOLLOWER.toString(); | ||
| String hostIp = ""; | ||
| if (peerInetAddress != null) { | ||
| String rawIp = peerInetAddress.getHostAddress(); | ||
| hostIp = rawIp.contains(":") ? "[" + rawIp + "]" : rawIp; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small suggestion: since we already have the |
||
| } | ||
| String roleEntry = normalizedAddress + ":" + role + ":" | ||
| + peer.getId().toString() + ":" + hostIp; | ||
| ratisRoles.add(roleEntry); | ||
| } | ||
| return ratisRoles; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,16 +104,16 @@ private SCMNodeInfo findLeaderNode(ScmClient scmClient) throws IOException { | |
| try { | ||
| List<String> roles = scmClient.getScmRoles(); | ||
| for (String role : roles) { | ||
| String[] parts = role.split(":"); | ||
| if (parts.length < 3 || !"LEADER".equalsIgnoreCase(parts[2])) { | ||
| String[] parts = HddsUtils.parseRatisRoleString(role); | ||
| if (!"LEADER".equalsIgnoreCase(parts[2])) { | ||
| continue; | ||
| } | ||
| String leaderHost = parts[0]; | ||
| String leaderIp = parts.length >= 5 ? parts[4] : null; | ||
| String leaderIp = parts[4]; | ||
| for (SCMNodeInfo node : nodes) { | ||
| String nodeHost = node.getScmClientAddress().split(":")[0]; | ||
| String nodeHost = HddsUtils.getHostName(node.getScmClientAddress()).orElse(""); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This correctly extracts the IPv6 host, but |
||
|
|
||
| if (matchesAddress(leaderHost, nodeHost) || (leaderIp != null && !leaderIp.isEmpty() && | ||
| if (matchesAddress(leaderHost, nodeHost) || (!leaderIp.isEmpty() && | ||
| matchesAddress(leaderIp, nodeHost))) { | ||
| return node; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,14 +17,12 @@ | |
|
|
||
| package org.apache.hadoop.ozone.admin.scm; | ||
|
|
||
| import static java.lang.System.err; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.apache.hadoop.hdds.HddsUtils; | ||
| import org.apache.hadoop.hdds.cli.HddsVersionProvider; | ||
| import org.apache.hadoop.hdds.scm.cli.ScmSubcommand; | ||
| import org.apache.hadoop.hdds.scm.client.ScmClient; | ||
|
|
@@ -73,10 +71,7 @@ public void execute(ScmClient scmClient) throws IOException { | |
| formattingCLIUtils.addHeaders(SCM_ROLES_HEADER); | ||
|
|
||
| for (String role : peerRoles) { | ||
| String[] roleItems = role.split(":"); | ||
| if (roleItems.length < 2) { | ||
| err.println("Invalid response received for ScmRatisRoles."); | ||
| } | ||
| String[] roleItems = HddsUtils.parseRatisRoleString(role); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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? |
||
| formattingCLIUtils.addLine(roleItems); | ||
| } | ||
| System.out.println(formattingCLIUtils.render()); | ||
|
|
@@ -92,20 +87,12 @@ private Map<String, Map<String, String>> parseScmRoles( | |
| Map<String, Map<String, String>> allRoles = new HashMap<>(); | ||
| for (String role : peerRoles) { | ||
| Map<String, String> roleDetails = new HashMap<>(); | ||
| String[] roles = role.split(":"); | ||
| if (roles.length < 2) { | ||
| err.println("Invalid response received for ScmRatisRoles."); | ||
| return Collections.emptyMap(); | ||
| } | ||
| // In case, there is no ratis, there is no ratis role. | ||
| // This will just print the hostname with ratis port as the address | ||
| roleDetails.put("address", roles[0].concat(":").concat(roles[1])); | ||
| if (roles.length == 5) { | ||
| roleDetails.put("raftPeerRole", roles[2]); | ||
| roleDetails.put("ID", roles[3]); | ||
| roleDetails.put("InetAddress", roles[4]); | ||
| } | ||
| allRoles.put(roles[0], roleDetails); | ||
| String[] fields = HddsUtils.parseRatisRoleString(role); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we preserve the existing |
||
| roleDetails.put("address", fields[0] + ":" + fields[1]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For IPv6 this produces an ambiguous address such as |
||
| roleDetails.put("raftPeerRole", fields[2]); | ||
| roleDetails.put("ID", fields[3]); | ||
| roleDetails.put("InetAddress", fields[4]); | ||
| allRoles.put(fields[0], roleDetails); | ||
| } | ||
| return allRoles; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's fix
HostAndPortclass as well:Otherwise
SCMNodeInfo.buildNodeInfo()can still produce invalid unbracketed authority.