This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_10x by this push:
     new a9b87593fae SOLR-17136: Add request classes for metrics and sys-info 
(#3955)
a9b87593fae is described below

commit a9b87593fae28ef7720e82864f1896798f330906
Author: igiguere <[email protected]>
AuthorDate: Sun Dec 28 12:54:49 2025 -0500

    SOLR-17136: Add request classes for metrics and sys-info (#3955)
    
    Having dedicated SolrRequest implementations for these APIs gives users
    a much better experience, and allows the server-side code to replace a
    number of GenericSolrRequest usages that were pretty ugly in the
    server's "core" module.
    
    Co-authored-by: Isabelle Giguere <[email protected]>
    Co-authored-by: Eric Pugh <[email protected]>
    Co-authored-by: Jason Gerlowski <[email protected]>
---
 .../SOLR-17136-replace-GenericSolrRequest.yml      |   7 +
 .../solr/client/api/model/NodeSystemResponse.java  | 126 +++++++++++
 .../src/java/org/apache/solr/cli/CLIUtils.java     |  18 +-
 .../src/java/org/apache/solr/cli/CreateTool.java   |  16 +-
 .../java/org/apache/solr/cli/HealthcheckTool.java  |  21 +-
 .../src/java/org/apache/solr/cli/StatusTool.java   |  47 ++--
 .../solr/cloud/api/collections/SplitShardCmd.java  |   8 +-
 .../solr/packagemanager/RepositoryManager.java     |  17 +-
 .../TestEmbeddedSolrServerAdminHandler.java        |  28 +--
 .../apache/solr/cloud/BasicDistributedZkTest.java  |  10 +-
 .../apache/solr/cloud/TestBaseStatsCacheCloud.java |  11 +-
 .../solr/handler/admin/AdminHandlersProxyTest.java |  46 ++--
 .../solr/handler/admin/MetricsHandlerTest.java     |  16 +-
 .../response/TestPrometheusResponseWriter.java     |  21 +-
 .../TestPrometheusResponseWriterCloud.java         |  12 +-
 .../apache/solr/security/MultiAuthPluginTest.java  |   9 +-
 .../security/jwt/JWTAuthPluginIntegrationTest.java |  11 +-
 .../solr/opentelemetry/TestDistributedTracing.java |   6 +-
 .../solr/opentelemetry/TestMetricExemplars.java    |  10 +-
 .../client/solrj/io/sql/DatabaseMetaDataImpl.java  |  14 +-
 .../solr/client/solrj/impl/NodeValueFetcher.java   |   6 +-
 .../solrj/impl/SolrClientNodeStateProvider.java    |   5 +-
 .../solr/client/solrj/request/MetricsRequest.java  |  57 +++++
 .../client/solrj/request/SystemInfoRequest.java    |  83 +++++++
 .../client/solrj/response/SystemInfoResponse.java  | 251 +++++++++++++++++++++
 .../apache/solr/common/params/CommonParams.java    |   1 +
 .../apache/solr/client/solrj/SolrExampleTests.java |   2 +-
 .../solrj/response/SystemInfoResponseTest.java     |  80 +++++++
 .../org/apache/solr/util/SolrJMetricTestUtils.java |  24 +-
 29 files changed, 722 insertions(+), 241 deletions(-)

diff --git a/changelog/unreleased/SOLR-17136-replace-GenericSolrRequest.yml 
b/changelog/unreleased/SOLR-17136-replace-GenericSolrRequest.yml
new file mode 100644
index 00000000000..4d20d1a613f
--- /dev/null
+++ b/changelog/unreleased/SOLR-17136-replace-GenericSolrRequest.yml
@@ -0,0 +1,7 @@
+title: Introduce new SolrJ SolrRequest classes for metrics and "system info" 
requests.
+type: added
+authors:
+- name: Isabelle Giguère
+links:
+- name: SOLR-17136
+  url: https://issues.apache.org/jira/browse/SOLR-17136
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/NodeSystemResponse.java 
b/solr/api/src/java/org/apache/solr/client/api/model/NodeSystemResponse.java
new file mode 100644
index 00000000000..09bc35f0867
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/NodeSystemResponse.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.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/** Response from /node/system */
+public class NodeSystemResponse extends SolrJerseyResponse {
+
+  @JsonProperty public String mode;
+  @JsonProperty public String zkHost;
+
+  @JsonProperty("solr_home")
+  public String solrHome;
+
+  @JsonProperty("core_root")
+  public String coreRoot;
+
+  @JsonProperty public String environment;
+
+  @JsonProperty(value = "environment_label")
+  public String environmentLabel;
+
+  @JsonProperty(value = "environment_color")
+  public String environmentColor;
+
+  @JsonProperty public String node;
+  @JsonProperty public Lucene lucene;
+  @JsonProperty public JVM jvm;
+  @JsonProperty public Security security;
+  @JsonProperty public GPU gpu;
+  @JsonProperty public Map<String, String> system;
+
+  /** /node/system/security */
+  public static class Security {
+    @JsonProperty public boolean tls;
+    @JsonProperty public String authenticationPlugin;
+    @JsonProperty public String authorizationPlugin;
+    @JsonProperty public String username;
+    @JsonProperty public List<String> roles;
+    @JsonProperty public List<String> permissions;
+  }
+
+  /** /node/system/lucene */
+  public static class Lucene {
+    @JsonProperty("solr-spec-version")
+    public String solrSpecVersion;
+
+    @JsonProperty("solr-impl-version")
+    public String solrImplVersion;
+
+    @JsonProperty("lucene-spec-version")
+    public String luceneSpecVersion;
+
+    @JsonProperty("lucene-impl-version")
+    public String luceneImplVersion;
+  }
+
+  /** /node/system/jvm */
+  public static class JVM extends Vendor {
+    @JsonProperty public int processors;
+    @JsonProperty public Vendor jre;
+    @JsonProperty public Vendor spec;
+    @JsonProperty public Vendor vm;
+    @JsonProperty public JvmJmx jmx;
+    @JsonProperty public JvmMemory memory;
+  }
+
+  public static class JvmMemory {
+    @JsonProperty public String free;
+    @JsonProperty public String total;
+    @JsonProperty public String max;
+    @JsonProperty public String used;
+    @JsonProperty public JvmMemoryRaw raw;
+  }
+
+  public static class JvmMemoryRaw extends MemoryRaw {
+    @JsonProperty public long max;
+
+    @JsonProperty("used%")
+    public double usedPercent;
+  }
+
+  public static class MemoryRaw {
+    @JsonProperty public long free;
+    @JsonProperty public long total;
+    @JsonProperty public long used;
+  }
+
+  public static class Vendor {
+    @JsonProperty public String name;
+    @JsonProperty public String vendor;
+    @JsonProperty public String version;
+  }
+
+  public static class JvmJmx {
+    @JsonProperty public String classpath;
+    @JsonProperty public Date startTime;
+    @JsonProperty public long upTimeMS;
+    @JsonProperty public List<String> commandLineArgs;
+  }
+
+  public static class GPU {
+    @JsonProperty public boolean available;
+    @JsonProperty public long count;
+    @JsonProperty public MemoryRaw memory;
+    @JsonProperty Map<String, Object> devices;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java 
b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
index 0520fab4e51..36baeb8357f 100644
--- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
+++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
@@ -19,7 +19,6 @@ package org.apache.solr.cli;
 
 import static org.apache.solr.common.SolrException.ErrorCode.FORBIDDEN;
 import static org.apache.solr.common.SolrException.ErrorCode.UNAUTHORIZED;
-import static org.apache.solr.common.params.CommonParams.SYSTEM_INFO_PATH;
 
 import java.io.IOException;
 import java.net.SocketException;
@@ -38,18 +37,17 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.commons.exec.OS;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.impl.SolrZkClientTimeout;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoresApi;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.NamedList;
 
@@ -249,12 +247,7 @@ public final class CLIUtils {
 
     try (SolrClient solrClient = getSolrClient(cli)) {
       // hit Solr to get system info
-      NamedList<Object> systemInfo =
-          solrClient.request(
-              new GenericSolrRequest(SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
-
-      // convert raw JSON into user-friendly output
-      Map<String, Object> status = StatusTool.reportStatus(systemInfo, 
solrClient);
+      Map<String, Object> status = StatusTool.reportStatus(solrClient);
       @SuppressWarnings("unchecked")
       Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
       if (cloud != null) {
@@ -357,9 +350,8 @@ public final class CLIUtils {
   }
 
   public static boolean isCloudMode(SolrClient solrClient) throws 
SolrServerException, IOException {
-    NamedList<Object> systemInfo =
-        solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, 
SYSTEM_INFO_PATH));
-    return "solrcloud".equals(systemInfo.get("mode"));
+    SystemInfoResponse sysResponse = new 
SystemInfoRequest().process(solrClient);
+    return "solrcloud".equals(sysResponse.getMode());
   }
 
   public static Path getConfigSetsDir(Path solrInstallDir) {
diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java 
b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
index b6790b4a4fa..d18d2e0c40e 100644
--- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
@@ -29,18 +29,17 @@ import org.apache.commons.cli.Options;
 import org.apache.commons.io.file.PathUtils;
 import org.apache.solr.cli.CommonCLIOptions.DefaultValues;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
 import org.apache.solr.client.solrj.response.CoreAdminResponse;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.client.solrj.response.json.JsonMapResponseParser;
 import org.apache.solr.cloud.ZkConfigSetService;
 import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.ConfigSetService;
 import org.noggit.CharArr;
@@ -157,14 +156,9 @@ public class CreateTool extends ToolBase {
     }
     printDefaultConfigsetWarningIfNecessary(cli);
 
-    String coreRootDirectory; // usually same as solr home, but not always
-
-    NamedList<?> systemInfo =
-        solrClient.request(
-            new GenericSolrRequest(SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
-
-    // convert raw JSON into user-friendly output
-    coreRootDirectory = (String) systemInfo.get("core_root");
+    SystemInfoResponse sysResponse = (new 
SystemInfoRequest()).process(solrClient);
+    // usually same as solr home, but not always
+    String coreRootDirectory = sysResponse.getCoreRoot();
 
     if (CLIUtils.safeCheckCoreExists(
         solrUrl, coreName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java 
b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
index ef0f61bebda..8745941cff3 100644
--- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
@@ -30,18 +30,16 @@ import java.util.Set;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.SolrQuery;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
 import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.util.NamedList;
 import org.noggit.CharArr;
 import org.noggit.JSONWriter;
 import org.slf4j.Logger;
@@ -168,15 +166,12 @@ public class HealthcheckTool extends ToolBase {
             try (var solrClient =
                 CLIUtils.getSolrClient(
                     r.getBaseUrl(), 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
-              NamedList<Object> systemInfo =
-                  solrClient.request(
-                      new GenericSolrRequest(
-                          SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
-              uptime =
-                  SolrCLI.uptime((Long) systemInfo._get(List.of("jvm", "jmx", 
"upTimeMS"), null));
-              String usedMemory = systemInfo._getStr(List.of("jvm", "memory", 
"used"), null);
-              String totalMemory = systemInfo._getStr(List.of("jvm", "memory", 
"total"), null);
-              memory = usedMemory + " of " + totalMemory;
+              SystemInfoResponse sysResponse = (new 
SystemInfoRequest()).process(solrClient);
+              uptime = SolrCLI.uptime(sysResponse.getJVMUpTimeMillis());
+              memory =
+                  sysResponse.getHumanReadableJVMMemoryUsed()
+                      + " of "
+                      + sysResponse.getHumanReadableJVMMemoryTotal();
             }
 
             // if we get here, we can trust the state
diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java 
b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
index 2f7422c054f..9fee5940a17 100644
--- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
@@ -31,10 +31,9 @@ import org.apache.commons.cli.OptionGroup;
 import org.apache.commons.cli.Options;
 import org.apache.solr.cli.SolrProcessManager.SolrProcess;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
-import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.URLUtil;
 import org.noggit.CharArr;
@@ -292,42 +291,30 @@ public class StatusTool extends ToolBase {
 
   public Map<String, Object> getStatus(String solrUrl, String credentials) 
throws Exception {
     try (var solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) {
-      return getStatus(solrClient);
+      Map<String, Object> status = reportStatus(solrClient);
+      return status;
     }
   }
 
-  public Map<String, Object> getStatus(SolrClient solrClient) throws Exception 
{
-    Map<String, Object> status;
-
-    NamedList<Object> systemInfo =
-        solrClient.request(
-            new GenericSolrRequest(SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
-    // convert raw JSON into user-friendly output
-    status = reportStatus(systemInfo, solrClient);
-
-    return status;
-  }
-
-  public static Map<String, Object> reportStatus(NamedList<Object> info, 
SolrClient solrClient)
-      throws Exception {
+  public static Map<String, Object> reportStatus(SolrClient solrClient) throws 
Exception {
     Map<String, Object> status = new LinkedHashMap<>();
+    SystemInfoResponse sysResponse = (new 
SystemInfoRequest()).process(solrClient);
+    status.put("solr_home", sysResponse.getSolrHome() != null ? 
sysResponse.getSolrHome() : "?");
+    status.put("version", sysResponse.getSolrImplVersion());
 
-    String solrHome = (String) info.get("solr_home");
-    status.put("solr_home", solrHome != null ? solrHome : "?");
-    status.put("version", info._getStr(List.of("lucene", "solr-impl-version"), 
null));
-    status.put("startTime", info._getStr(List.of("jvm", "jmx", "startTime"), 
null));
-    status.put("uptime", SolrCLI.uptime((Long) info._get(List.of("jvm", "jmx", 
"upTimeMS"), null)));
+    status.put("startTime", sysResponse.getJVMStartTime());
+    status.put("uptime", sysResponse.getJVMUpTimeMillis());
 
-    String usedMemory = info._getStr(List.of("jvm", "memory", "used"), null);
-    String totalMemory = info._getStr(List.of("jvm", "memory", "total"), null);
-    status.put("memory", usedMemory + " of " + totalMemory);
+    status.put(
+        "memory",
+        sysResponse.getHumanReadableJVMMemoryUsed()
+            + " of "
+            + sysResponse.getHumanReadableJVMMemoryTotal());
 
     // if this is a Solr in solrcloud mode, gather some basic cluster info
-    if ("solrcloud".equals(info.get("mode"))) {
-      String zkHost = (String) info.get("zkHost");
-      status.put("cloud", getCloudStatus(solrClient, zkHost));
+    if ("solrcloud".equals(sysResponse.getMode())) {
+      status.put("cloud", getCloudStatus(solrClient, sysResponse.getZkHost()));
     }
-
     return status;
   }
 
diff --git 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
index 8548a0bc17a..c799ec98969 100644
--- 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
+++ 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/SplitShardCmd.java
@@ -39,13 +39,12 @@ import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.cloud.DistribStateManager;
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.cloud.VersionedData;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.cloud.DistributedClusterStateUpdater;
 import org.apache.solr.cloud.Overseer;
 import 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ShardRequestTracker;
@@ -869,10 +868,7 @@ public class SplitShardCmd implements 
CollApiCmds.CollectionApiCommand {
         new ModifiableSolrParams()
             .add("key", indexSizeMetricName)
             .add("key", freeDiskSpaceMetricName);
-    SolrResponse rsp =
-        new GenericSolrRequest(
-                SolrRequest.METHOD.GET, "/admin/metrics", 
SolrRequest.SolrRequestType.ADMIN, params)
-            .process(cloudManager.getSolrClient());
+    SolrResponse rsp = new 
MetricsRequest(params).process(cloudManager.getSolrClient());
 
     Number size = (Number) rsp.getResponse()._get(List.of("metrics", 
indexSizeMetricName), null);
     if (size == null) {
diff --git 
a/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java 
b/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
index b56d6441709..7e5c0e16b29 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
@@ -18,7 +18,6 @@
 package org.apache.solr.packagemanager;
 
 import static org.apache.solr.cli.SolrCLI.printGreen;
-import static org.apache.solr.common.params.CommonParams.SYSTEM_INFO_PATH;
 import static org.apache.solr.packagemanager.PackageUtils.getMapper;
 
 import java.io.IOException;
@@ -47,10 +46,13 @@ import org.apache.solr.client.solrj.request.FileStoreApi;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.GenericV2SolrRequest;
 import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
 import org.apache.solr.client.solrj.request.beans.PackagePayload;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.filestore.ClusterFileStore;
@@ -142,14 +144,17 @@ public class RepositoryManager {
 
   public void addKey(byte[] key, String destinationKeyFilename) throws 
Exception {
     // get solr_home directory from info servlet
-    NamedList<Object> systemInfo =
-        solrClient.request(
-            new GenericSolrRequest(SolrRequest.METHOD.GET, "/solr" + 
SYSTEM_INFO_PATH));
-    String solrHome = (String) systemInfo.get("solr_home");
+    // This method is only called from PackageTool ("add-repo", or "add-key"), 
where the Solr URL is
+    // normalized to remove the /solr path part
+    // So might as well ping the V2 API "/node/system" instead.
+    // Otherwise, this SystemInfoRequest constructor would need to set the full
+    // /solr/admin/info/system path
+    SystemInfoResponse sysResponse =
+        new 
SystemInfoRequest(CommonParams.V2_SYSTEM_INFO_PATH).process(solrClient);
 
     // put the public key into package store's trusted key store and request a 
sync.
     String path = ClusterFileStore.KEYS_DIR + "/" + destinationKeyFilename;
-    PackageUtils.uploadKey(key, path, Path.of(solrHome));
+    PackageUtils.uploadKey(key, path, Path.of(sysResponse.getSolrHome()));
     final var syncRequest = new FileStoreApi.SyncFile(path);
     final var syncResponse = syncRequest.process(solrClient);
     final var status = syncResponse.responseHeader.status;
diff --git 
a/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerAdminHandler.java
 
b/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerAdminHandler.java
index 7118520fa12..33be625e1ad 100644
--- 
a/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerAdminHandler.java
+++ 
b/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerAdminHandler.java
@@ -18,12 +18,9 @@ package org.apache.solr.client.solrj.embedded;
 
 import java.io.IOException;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.response.QueryResponse;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.NamedList;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.core.NodeConfig;
 import org.junit.Test;
 
@@ -39,25 +36,8 @@ public class TestEmbeddedSolrServerAdminHandler extends 
SolrTestCaseJ4 {
 
     try (final EmbeddedSolrServer server = new EmbeddedSolrServer(config, 
"collection1")) {
       final SystemInfoRequest info = new SystemInfoRequest();
-      final NamedList<?> response = server.request(info);
-      assertTrue(response.size() > 0);
-    }
-  }
-
-  private static class SystemInfoRequest extends SolrRequest<QueryResponse> {
-
-    public SystemInfoRequest() {
-      super(METHOD.GET, "/admin/info/system", 
SolrRequest.SolrRequestType.ADMIN);
-    }
-
-    @Override
-    public SolrParams getParams() {
-      return new ModifiableSolrParams();
-    }
-
-    @Override
-    protected QueryResponse createResponse(final NamedList<Object> namedList) {
-      return new QueryResponse();
+      final SystemInfoResponse response = info.process(server);
+      assertTrue(response.getResponse().size() > 0);
     }
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/BasicDistributedZkTest.java 
b/solr/core/src/test/org/apache/solr/cloud/BasicDistributedZkTest.java
index 1732cd487d0..920b1aa2f3b 100644
--- a/solr/core/src/test/org/apache/solr/cloud/BasicDistributedZkTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/BasicDistributedZkTest.java
@@ -43,14 +43,13 @@ import org.apache.lucene.util.IOUtils;
 import org.apache.solr.JSONTestUtil;
 import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
 import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.CoreAdminRequest.Create;
 import org.apache.solr.client.solrj.request.CoreAdminRequest.Unload;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.request.SolrQuery;
 import org.apache.solr.client.solrj.request.StreamingUpdateRequest;
@@ -1283,12 +1282,7 @@ public class BasicDistributedZkTest extends 
AbstractFullDistribZkTestBase {
             .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
             .withSocketTimeout(60000, TimeUnit.MILLISECONDS)
             .build()) {
-      var req =
-          new GenericSolrRequest(
-              SolrRequest.METHOD.GET,
-              "/admin/metrics",
-              SolrRequest.SolrRequestType.ADMIN,
-              SolrParams.of("wt", "prometheus"));
+      var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
       req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
       NamedList<Object> resp = client.request(req);
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/TestBaseStatsCacheCloud.java 
b/solr/core/src/test/org/apache/solr/cloud/TestBaseStatsCacheCloud.java
index 11cff8c431c..5fc8bf904a8 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestBaseStatsCacheCloud.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestBaseStatsCacheCloud.java
@@ -22,12 +22,10 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest.METHOD;
-import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.request.UpdateRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.client.solrj.response.QueryResponse;
@@ -140,12 +138,7 @@ public abstract class TestBaseStatsCacheCloud extends 
SolrCloudTestCase {
     StatsCache.StatsCacheMetrics statsCacheMetrics = new 
StatsCache.StatsCacheMetrics();
     for (JettySolrRunner jettySolrRunner : cluster.getJettySolrRunners()) {
       try (SolrClient client = 
getHttpSolrClient(jettySolrRunner.getBaseUrl().toString())) {
-        var req =
-            new GenericSolrRequest(
-                METHOD.GET,
-                "/admin/metrics",
-                SolrRequestType.ADMIN,
-                SolrParams.of("wt", "prometheus"));
+        var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
         req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
         NamedList<Object> resp = client.request(req);
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/AdminHandlersProxyTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/AdminHandlersProxyTest.java
index ef39b046a6f..14411a38876 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/AdminHandlersProxyTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/AdminHandlersProxyTest.java
@@ -20,13 +20,10 @@ package org.apache.solr.handler.admin;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Set;
-import org.apache.http.client.HttpClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
-import org.apache.solr.client.solrj.response.SimpleSolrResponse;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.MapSolrParams;
@@ -36,7 +33,6 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class AdminHandlersProxyTest extends SolrCloudTestCase {
-  private HttpClient httpClient;
   private CloudSolrClient solrClient;
 
   @BeforeClass
@@ -50,19 +46,14 @@ public class AdminHandlersProxyTest extends 
SolrCloudTestCase {
   public void setUp() throws Exception {
     super.setUp();
     solrClient = cluster.getSolrClient();
-    httpClient = ((CloudLegacySolrClient) solrClient).getHttpClient();
   }
 
   @Test
   public void proxySystemInfoHandlerAllNodes() throws IOException, 
SolrServerException {
     MapSolrParams params = new MapSolrParams(Collections.singletonMap("nodes", 
"all"));
-    GenericSolrRequest req =
-        new GenericSolrRequest(
-            SolrRequest.METHOD.GET,
-            "/admin/info/system",
-            SolrRequest.SolrRequestType.ADMIN,
-            params);
-    SimpleSolrResponse rsp = req.process(solrClient, null);
+
+    SystemInfoRequest req = new SystemInfoRequest(params);
+    SystemInfoResponse rsp = req.process(solrClient, null);
     NamedList<Object> nl = rsp.getResponse();
     assertEquals(3, nl.size());
     assertTrue(nl.getName(1).endsWith("_solr"));
@@ -75,13 +66,8 @@ public class AdminHandlersProxyTest extends 
SolrCloudTestCase {
   public void proxySystemInfoHandlerNonExistingNode() throws IOException, 
SolrServerException {
     MapSolrParams params =
         new MapSolrParams(Collections.singletonMap("nodes", 
"example.com:1234_solr"));
-    GenericSolrRequest req =
-        new GenericSolrRequest(
-            SolrRequest.METHOD.GET,
-            "/admin/info/system",
-            SolrRequest.SolrRequestType.ADMIN,
-            params);
-    SimpleSolrResponse rsp = req.process(solrClient, null);
+    SystemInfoRequest req = new SystemInfoRequest(params);
+    SystemInfoResponse rsp = req.process(solrClient, null);
   }
 
   @Test
@@ -91,22 +77,16 @@ public class AdminHandlersProxyTest extends 
SolrCloudTestCase {
     nodes.forEach(
         node -> {
           MapSolrParams params = new 
MapSolrParams(Collections.singletonMap("nodes", node));
-          GenericSolrRequest req =
-              new GenericSolrRequest(
-                  SolrRequest.METHOD.GET,
-                  "/admin/info/system",
-                  SolrRequest.SolrRequestType.ADMIN,
-                  params);
-          SimpleSolrResponse rsp = null;
+          SystemInfoRequest req = new SystemInfoRequest(params);
           try {
-            rsp = req.process(solrClient, null);
+            SystemInfoResponse rsp = req.process(solrClient, null);
+            NamedList<Object> nl = rsp.getResponse();
+            assertEquals(2, nl.size());
+            assertEquals("solrcloud", rsp.getMode());
+            assertEquals(nl.getName(1), rsp.getNode());
           } catch (Exception e) {
             fail("Exception while proxying request to node " + node);
           }
-          NamedList<Object> nl = rsp.getResponse();
-          assertEquals(2, nl.size());
-          assertEquals("solrcloud", ((NamedList) 
nl.get(nl.getName(1))).get("mode"));
-          assertEquals(nl.getName(1), ((NamedList) 
nl.get(nl.getName(1))).get("node"));
         });
   }
 }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
index f2ea61a9fb5..7cf04d54375 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
@@ -51,7 +51,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.METRIC_NAME_PARAM,
@@ -78,7 +78,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.METRIC_NAME_PARAM,
@@ -103,7 +103,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.METRIC_NAME_PARAM,
@@ -124,7 +124,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.CATEGORY_PARAM,
@@ -154,7 +154,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.CATEGORY_PARAM,
@@ -186,7 +186,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.CORE_PARAM,
@@ -208,7 +208,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.CORE_PARAM,
@@ -243,7 +243,7 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     handler.handleRequestBody(
         req(
             CommonParams.QT,
-            "/admin/metrics",
+            CommonParams.METRICS_PATH,
             CommonParams.WT,
             MetricsHandler.PROMETHEUS_METRICS_WT,
             MetricsHandler.CATEGORY_PARAM,
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
index 2c86fc41bfb..fc2754da254 100644
--- 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
+++ 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java
@@ -29,9 +29,7 @@ import java.util.stream.Collectors;
 import org.apache.lucene.tests.util.LuceneTestCase;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest.METHOD;
-import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.NamedList;
@@ -74,7 +72,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
   public void testPrometheusStructureOutput() throws Exception {
     ModifiableSolrParams params = new ModifiableSolrParams();
     params.set("wt", "prometheus");
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest(params);
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
     try (SolrClient adminClient = 
getHttpSolrClient(solrClientTestRule.getBaseUrl())) {
@@ -122,8 +120,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
   @Test
   public void testAcceptHeaderOpenMetricsFormat() throws Exception {
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest();
 
     req.setResponseParser(new InputStreamResponseParser(null));
 
@@ -143,8 +140,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
   @Test
   public void testWtParameterOpenMetricsFormat() throws Exception {
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest();
 
     req.setResponseParser(new InputStreamResponseParser("openmetrics"));
 
@@ -162,8 +158,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
   @Test
   public void testDefaultPrometheusFormat() throws Exception {
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest();
 
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
@@ -181,8 +176,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
   @Test
   public void testDefaultPrometheusFormatNoWtParam() throws Exception {
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest();
 
     req.setResponseParser(new InputStreamResponseParser(null));
 
@@ -200,8 +194,7 @@ public class TestPrometheusResponseWriter extends 
SolrTestCaseJ4 {
 
   @Test
   public void testUnsupportedMetricsFormat() throws Exception {
-    ModifiableSolrParams params = new ModifiableSolrParams();
-    var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest();
 
     req.setResponseParser(new InputStreamResponseParser("unknownFormat"));
 
diff --git 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
index cd5149ca37f..ffc254723e1 100644
--- 
a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
+++ 
b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriterCloud.java
@@ -19,10 +19,8 @@ package org.apache.solr.response;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest.METHOD;
-import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.request.SolrQuery;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.cloud.SolrCloudTestCase;
@@ -69,9 +67,7 @@ public class TestPrometheusResponseWriterCloud extends 
SolrCloudTestCase {
     SolrQuery query = new SolrQuery("*:*");
     solrClient.query("collection1", query);
 
-    var req =
-        new GenericSolrRequest(
-            METHOD.GET, "/admin/metrics", SolrRequestType.ADMIN, 
SolrParams.of("wt", "prometheus"));
+    var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
     NamedList<Object> resp = solrClient.request(req);
@@ -101,9 +97,7 @@ public class TestPrometheusResponseWriterCloud extends 
SolrCloudTestCase {
     solrClient.query("collection1", query);
     solrClient.query("collection2", query);
 
-    var req =
-        new GenericSolrRequest(
-            METHOD.GET, "/admin/metrics", SolrRequestType.ADMIN, 
SolrParams.of("wt", "prometheus"));
+    var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
     NamedList<Object> resp = solrClient.request(req);
diff --git 
a/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java 
b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
index f04bd268cda..8a9c2ca9205 100644
--- a/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
+++ b/solr/core/src/test/org/apache/solr/security/MultiAuthPluginTest.java
@@ -48,6 +48,7 @@ import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.embedded.JettySolrRunner;
@@ -132,12 +133,12 @@ public class MultiAuthPluginTest extends SolrTestCaseJ4 {
           pass);
 
       // anonymous requests are blocked by all plugins
-      int statusCode = doHttpGetAnonymous(httpClient, baseUrl + 
"/admin/info/system");
+      int statusCode = doHttpGetAnonymous(httpClient, baseUrl + 
CommonParams.SYSTEM_INFO_PATH);
       assertEquals("anonymous get succeeded but should not have", 401, 
statusCode);
       // update blockUnknown to allow anonymous for the basic plugin
       String command = "{\n" + "'set-property': { 'basic': 
{'blockUnknown':false} }\n" + "}";
       doHttpPost(httpClient, baseUrl + authcPrefix, command, user, pass, 200);
-      statusCode = doHttpGetAnonymous(httpClient, baseUrl + 
"/admin/info/system");
+      statusCode = doHttpGetAnonymous(httpClient, baseUrl + 
CommonParams.SYSTEM_INFO_PATH);
       assertEquals("anonymous get failed but should have succeeded", 200, 
statusCode);
 
       // For the multi-auth plugin, every command is wrapped with an object 
that identifies the
@@ -474,7 +475,7 @@ public class MultiAuthPluginTest extends SolrTestCaseJ4 {
       securityConfHandler.securityConfEdited();
 
       // Pretend to send unauthorized AJAX request
-      HttpGet httpGet = new HttpGet(baseUrl + "/admin/info/system");
+      HttpGet httpGet = new HttpGet(baseUrl + CommonParams.SYSTEM_INFO_PATH);
       httpGet.addHeader(new BasicHeader("X-Requested-With", "XMLHttpRequest"));
 
       HttpResponse response = httpClient.execute(httpGet);
@@ -515,7 +516,7 @@ public class MultiAuthPluginTest extends SolrTestCaseJ4 {
 
   private void verifyWWWAuthenticateHeaders(HttpClient httpClient, String 
baseUrl)
       throws Exception {
-    HttpGet httpGet = new HttpGet(baseUrl + "/admin/info/system");
+    HttpGet httpGet = new HttpGet(baseUrl + CommonParams.SYSTEM_INFO_PATH);
     HttpResponse response = httpClient.execute(httpGet);
     Header[] headers = response.getHeaders(HttpHeaders.WWW_AUTHENTICATE);
     List<String> actualSchemes =
diff --git 
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
index 1650fbb4788..dc281359b8c 100644
--- 
a/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
@@ -61,6 +61,7 @@ import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.cloud.MiniSolrCloudCluster;
 import org.apache.solr.cloud.SolrCloudAuthTestCase;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.Pair;
 import org.apache.solr.common.util.TimeSource;
 import org.apache.solr.embedded.JettySolrRunner;
@@ -142,11 +143,11 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     String baseUrl = 
myCluster.getRandomJetty(random()).getBaseUrl().toString();
 
     // First attempt without token fails
-    Map<String, String> headers = getHeaders(baseUrl + "/admin/info/system", 
null);
+    Map<String, String> headers = getHeaders(baseUrl + 
CommonParams.SYSTEM_INFO_PATH, null);
     assertEquals("Should have received 401 code", "401", headers.get("code"));
 
     // Second attempt with token from Oauth mock server succeeds
-    headers = getHeaders(baseUrl + "/admin/info/system", mockOAuthToken);
+    headers = getHeaders(baseUrl + CommonParams.SYSTEM_INFO_PATH, 
mockOAuthToken);
     assertEquals("200", headers.get("code"));
     myCluster.shutdown();
   }
@@ -162,10 +163,10 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     String baseUrl = 
myCluster.getRandomJetty(random()).getBaseUrl().toString();
 
     // No token fails
-    assertThrows(IOException.class, () -> get(baseUrl + "/admin/info/system", 
null));
+    assertThrows(IOException.class, () -> get(baseUrl + 
CommonParams.SYSTEM_INFO_PATH, null));
 
     // Validate X-Solr-AuthData headers
-    Map<String, String> headers = getHeaders(baseUrl + "/admin/info/system", 
null);
+    Map<String, String> headers = getHeaders(baseUrl + 
CommonParams.SYSTEM_INFO_PATH, null);
     assertEquals("Should have received 401 code", "401", headers.get("code"));
     assertEquals("Bearer realm=\"my-solr-jwt\"", 
headers.get("WWW-Authenticate"));
     String authData = new 
String(Base64.getDecoder().decode(headers.get("X-Solr-AuthData")), UTF_8);
@@ -188,7 +189,7 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
         
configureClusterStaticKeys("jwt_plugin_jwk_security_blockUnknownFalse.json");
     String baseUrl = 
myCluster.getRandomJetty(random()).getBaseUrl().toString();
 
-    Map<String, String> headers = getHeaders(baseUrl + "/admin/info/system", 
null);
+    Map<String, String> headers = getHeaders(baseUrl + 
CommonParams.SYSTEM_INFO_PATH, null);
     assertEquals("Should have received 401 code", "401", headers.get("code"));
     assertEquals(
         "Bearer realm=\"my-solr-jwt-blockunknown-false\"", 
headers.get("WWW-Authenticate"));
diff --git 
a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java
 
b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java
index bc90908bc6d..97916c007ca 100644
--- 
a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java
+++ 
b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestDistributedTracing.java
@@ -31,11 +31,10 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.request.SolrQuery;
 import org.apache.solr.client.solrj.request.V2Request;
 import org.apache.solr.client.solrj.response.CollectionAdminResponse;
@@ -136,8 +135,7 @@ public class TestDistributedTracing extends 
SolrCloudTestCase {
   public void testAdminApi() throws Exception {
     CloudSolrClient cloudClient = cluster.getSolrClient();
 
-    GenericSolrRequest request =
-        new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN);
+    MetricsRequest request = new MetricsRequest();
     request.setResponseParser(new 
InputStreamResponseParser(PROMETHEUS_METRICS_WT));
     NamedList<Object> rsp = cloudClient.request(request);
     ((InputStream) rsp.get("stream")).close();
diff --git 
a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestMetricExemplars.java
 
b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestMetricExemplars.java
index 0a842d57915..6eaf0c07e17 100644
--- 
a/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestMetricExemplars.java
+++ 
b/solr/modules/opentelemetry/src/test/org/apache/solr/opentelemetry/TestMetricExemplars.java
@@ -23,10 +23,9 @@ import io.opentelemetry.api.GlobalOpenTelemetry;
 import io.opentelemetry.api.trace.TracerProvider;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.params.ModifiableSolrParams;
@@ -79,12 +78,7 @@ public class TestMetricExemplars extends SolrCloudTestCase {
     var spans = getAndClearSpans();
     var expectedTrace = getRootTraceId(spans);
 
-    var req =
-        new GenericSolrRequest(
-            SolrRequest.METHOD.GET,
-            "/admin/metrics",
-            SolrRequest.SolrRequestType.ADMIN,
-            new ModifiableSolrParams().set("wt", "openmetrics"));
+    var req = new MetricsRequest(new ModifiableSolrParams().set("wt", 
"openmetrics"));
     req.setResponseParser(new InputStreamResponseParser("openmetrics"));
     NamedList<Object> resp = cloudClient.request(req);
 
diff --git 
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/sql/DatabaseMetaDataImpl.java
 
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/sql/DatabaseMetaDataImpl.java
index 643b51b57b1..cde6a04b068 100644
--- 
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/sql/DatabaseMetaDataImpl.java
+++ 
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/sql/DatabaseMetaDataImpl.java
@@ -28,10 +28,9 @@ import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
-import org.apache.solr.client.solrj.request.SolrQuery;
-import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
 import org.apache.solr.common.cloud.ClusterState;
-import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.Utils;
 
 class DatabaseMetaDataImpl implements DatabaseMetaData {
@@ -111,8 +110,6 @@ class DatabaseMetaDataImpl implements DatabaseMetaData {
   @Override
   public String getDatabaseProductVersion() throws SQLException {
     // Returns the version for the first live node in the Solr cluster.
-    SolrQuery sysQuery = new SolrQuery();
-    sysQuery.setRequestHandler("/admin/info/system");
 
     CloudSolrClient cloudSolrClient = this.connection.getClient();
     Set<String> liveNodes = cloudSolrClient.getClusterState().getLiveNodes();
@@ -126,9 +123,10 @@ class DatabaseMetaDataImpl implements DatabaseMetaData {
         String nodeURL = Utils.getBaseUrlForNodeName(node, urlScheme);
         solrClient = new HttpJettySolrClient.Builder(nodeURL).build();
 
-        QueryResponse rsp = solrClient.query(sysQuery);
-        return String.valueOf(
-            ((SimpleOrderedMap) 
rsp.getResponse().get("lucene")).get("solr-spec-version"));
+        SystemInfoRequest req = new SystemInfoRequest();
+        SystemInfoResponse resp = req.process(solrClient);
+
+        return resp.getSolrSpecVersion();
       } catch (SolrServerException | IOException ignore) {
         return "";
       } finally {
diff --git 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
index 868cbfd2926..3dd91fca5d3 100644
--- 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
+++ 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/NodeValueFetcher.java
@@ -30,9 +30,7 @@ import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
-import org.apache.solr.client.solrj.SolrRequest.METHOD;
-import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
 import org.apache.solr.common.SolrException;
@@ -176,7 +174,7 @@ public class NodeValueFetcher {
     params.add("name", StrUtils.join(uniqueMetricNames, ','));
 
     try {
-      var req = new GenericSolrRequest(METHOD.GET, "/admin/metrics", 
SolrRequestType.ADMIN, params);
+      var req = new MetricsRequest(params);
       req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
       String baseUrl =
diff --git 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/SolrClientNodeStateProvider.java
 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/SolrClientNodeStateProvider.java
index cc8365067ae..ef6bb5dbd21 100644
--- 
a/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/SolrClientNodeStateProvider.java
+++ 
b/solr/solrj-zookeeper/src/java/org/apache/solr/client/solrj/impl/SolrClientNodeStateProvider.java
@@ -34,6 +34,7 @@ import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.cloud.NodeStateProvider;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.client.solrj.response.JavaBinResponseParser;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
@@ -212,9 +213,7 @@ public class SolrClientNodeStateProvider implements 
NodeStateProvider, MapWriter
     params.add("wt", "prometheus");
     params.add("name", String.join(",", metricNames));
 
-    var req =
-        new GenericSolrRequest(
-            SolrRequest.METHOD.GET, "/admin/metrics", 
SolrRequest.SolrRequestType.ADMIN, params);
+    var req = new MetricsRequest(params);
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
     String baseUrl =
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/MetricsRequest.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/MetricsRequest.java
new file mode 100644
index 00000000000..82c10659586
--- /dev/null
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/MetricsRequest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.solr.client.solrj.request;
+
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.solrj.response.SolrResponseBase;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+
+/** Request to "/admin/metrics" */
+public class MetricsRequest extends SolrRequest<SolrResponse> {
+
+  private static final long serialVersionUID = 1L;
+
+  private final SolrParams params;
+
+  /** Request to "/admin/metrics" by default, without params */
+  public MetricsRequest() {
+    this(new ModifiableSolrParams());
+  }
+
+  /**
+   * @param params the Solr parameters to use for this request.
+   */
+  public MetricsRequest(SolrParams params) {
+    super(METHOD.GET, CommonParams.METRICS_PATH, SolrRequestType.ADMIN);
+    this.params = params;
+  }
+
+  @Override
+  public SolrParams getParams() {
+    return params;
+  }
+
+  @Override
+  protected SolrResponse createResponse(NamedList<Object> namedList) {
+    SolrResponseBase resp = new SolrResponseBase();
+    return (SolrResponse) resp;
+  }
+}
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/SystemInfoRequest.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/SystemInfoRequest.java
new file mode 100644
index 00000000000..345c0650256
--- /dev/null
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/SystemInfoRequest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.solr.client.solrj.request;
+
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.response.SystemInfoResponse;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+
+/** Class to get a system info response. */
+public class SystemInfoRequest extends SolrRequest<SystemInfoResponse> {
+
+  private static final long serialVersionUID = 1L;
+
+  private final SolrParams params;
+
+  /** Request to "/admin/info/system" by default, without params. */
+  public SystemInfoRequest() {
+    // TODO: support V2 by default.  Requires refactoring throughout the CLI 
tools, at least
+    this(CommonParams.SYSTEM_INFO_PATH);
+  }
+
+  /**
+   * @param path the HTTP path to use for this request. Supports V1 
"/admin/info/system" (default)
+   *     or V2 "/node/system"
+   */
+  public SystemInfoRequest(String path) {
+    this(path, new ModifiableSolrParams());
+  }
+
+  /**
+   * @param params the Solr parameters to use for this request.
+   */
+  public SystemInfoRequest(SolrParams params) {
+    this(CommonParams.SYSTEM_INFO_PATH, params);
+  }
+
+  /**
+   * @param path the HTTP path to use for this request. Supports V1 
"/admin/info/system" (default)
+   *     or V2 "/node/system"
+   * @param params query parameter names and values for making this request.
+   */
+  public SystemInfoRequest(String path, SolrParams params) {
+    super(METHOD.GET, path, SolrRequestType.ADMIN);
+    this.params = params;
+  }
+
+  @Override
+  public SolrParams getParams() {
+    return params;
+  }
+
+  @Override
+  protected SystemInfoResponse createResponse(NamedList<Object> namedList) {
+    return new SystemInfoResponse(namedList);
+  }
+
+  @Override
+  public ApiVersion getApiVersion() {
+    if (CommonParams.SYSTEM_INFO_PATH.equals(getPath())) {
+      // (/solr) /admin/info/system
+      return ApiVersion.V1;
+    }
+    // Ref. org.apache.solr.handler.admin.api.NodeSystemInfoAPI : /node/system
+    return ApiVersion.V2;
+  }
+}
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/response/SystemInfoResponse.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/response/SystemInfoResponse.java
new file mode 100644
index 00000000000..8dbea63118b
--- /dev/null
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/response/SystemInfoResponse.java
@@ -0,0 +1,251 @@
+/*
+ * 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.solr.client.solrj.response;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.apache.solr.client.api.model.NodeSystemResponse;
+import org.apache.solr.client.solrj.request.json.JacksonContentWriter;
+import org.apache.solr.common.util.NamedList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** This class holds the response from V1 "/admin/info/system" or V2 
"/node/system" */
+public class SystemInfoResponse extends SolrResponseBase {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static final long serialVersionUID = 1L;
+
+  private final Map<String, NodeSystemResponse> nodesInfo = new HashMap<>();
+
+  public SystemInfoResponse(NamedList<Object> namedList) {
+    if (namedList == null) throw new IllegalArgumentException("Null NamedList 
is not allowed.");
+    setResponse(namedList);
+  }
+
+  @Override
+  public void setResponse(NamedList<Object> response) {
+    if (getResponse() == null) super.setResponse(response);
+    if (getResponse().get("node") == null) {
+      // multi-nodes response, NamedList of 
"host:port_solr"->NodeSystemResponse
+      for (Entry<String, Object> node : getResponse()) {
+        if (node.getKey().endsWith("_solr")) {
+          nodesInfo.put(
+              node.getKey(),
+              JacksonContentWriter.DEFAULT_MAPPER.convertValue(
+                  node.getValue(), NodeSystemResponse.class));
+        }
+      }
+    } else {
+      // single-node response
+      nodesInfo.put(
+          getResponse().get("node").toString(),
+          JacksonContentWriter.DEFAULT_MAPPER.convertValue(
+              getResponse(), NodeSystemResponse.class));
+    }
+  }
+
+  /** Get the mode from a single node system info */
+  public String getMode() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().orElseThrow().mode;
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 'getAllModes', or 
'getModeForNode(String)'.");
+    }
+  }
+
+  /** Get all modes, per node */
+  public Map<String, String> getAllModes() {
+    Map<String, String> allModes = new HashMap<>();
+    nodesInfo.entrySet().stream().forEach(e -> allModes.put(e.getKey(), 
e.getValue().mode));
+    return allModes;
+  }
+
+  /** Get the mode for the given node name */
+  public String getModeForNode(String node) {
+    return nodesInfo.get(node).mode;
+  }
+
+  /** Get the ZK host from a single node system info */
+  public String getZkHost() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().orElseThrow().zkHost;
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 'getAllZkHosts', 
or 'getZkHostForNode(String)'.");
+    }
+  }
+
+  /** Get all ZK hosts, per node */
+  public Map<String, String> getAllZkHosts() {
+    Map<String, String> allModes = new HashMap<>();
+    nodesInfo.entrySet().stream().forEach(e -> allModes.put(e.getKey(), 
e.getValue().zkHost));
+    return allModes;
+  }
+
+  /** Get the ZK host for the given node name */
+  public String getZkHostForNode(String node) {
+    return nodesInfo.get(node).zkHost;
+  }
+
+  /** Get the Solr home from a single node system info */
+  public String getSolrHome() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().orElseThrow().solrHome;
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 'getAllSolrHomes', 
or 'getSolrHomeForNode(String)'.");
+    }
+  }
+
+  /** Get all Solr homes, per node */
+  public Map<String, String> getAllSolrHomes() {
+    Map<String, String> allModes = new HashMap<>();
+    nodesInfo.entrySet().stream().forEach(e -> allModes.put(e.getKey(), 
e.getValue().solrHome));
+    return allModes;
+  }
+
+  /** Get the Solr home for the given node name */
+  public String getSolrHomeForNode(String node) {
+    return nodesInfo.get(node).solrHome;
+  }
+
+  /** Get the core root from a single node system info */
+  public String getCoreRoot() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().orElseThrow().coreRoot;
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 'getAllCoreRoots', 
or 'getCoreRootForNode(String)'.");
+    }
+  }
+
+  /** Get all core roots, per node */
+  public Map<String, String> getAllCoreRoots() {
+    Map<String, String> allModes = new HashMap<>();
+    nodesInfo.entrySet().stream().forEach(e -> allModes.put(e.getKey(), 
e.getValue().coreRoot));
+    return allModes;
+  }
+
+  /** Get the core root for the given node name */
+  public String getCoreRootForNode(String node) {
+    return nodesInfo.get(node).coreRoot;
+  }
+
+  /** Get the node name from a single node system info */
+  public String getNode() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().orElseThrow().node;
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 'getAllNodes', or 
'getNodeForSolrHome(String)', or 'getNodeForCoreRoot(String)'.");
+    }
+  }
+
+  /** Get all nodes names */
+  public Set<String> getAllNodes() {
+    return nodesInfo.keySet();
+  }
+
+  /** Get the node name for the given Solr home */
+  public String getNodeForSolrHome(String solrHome) {
+    return nodesInfo.values().stream()
+        .filter(v -> solrHome.equals(v.solrHome))
+        .map(v -> v.node)
+        .findFirst()
+        .get();
+  }
+
+  /** Get the node name for the given core root */
+  public String getNodeForCoreRoot(String coreRoot) {
+    return nodesInfo.values().stream()
+        .filter(v -> coreRoot.equals(v.coreRoot))
+        .map(v -> v.node)
+        .findFirst()
+        .get();
+  }
+
+  /** Get the {@code NodeSystemResponse} for a single node */
+  public NodeSystemResponse getNodeResponse() {
+    if (nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().get();
+    } else {
+      throw new UnsupportedOperationException(
+          "Multiple nodes system info available, use method 
'getAllNodeResponses', or 'getNodeResponseForNode(String)'.");
+    }
+  }
+
+  /** Get all {@code NodeSystemResponse}s */
+  public Map<String, NodeSystemResponse> getAllNodeResponses() {
+    return nodesInfo;
+  }
+
+  /** Get the {@code NodeSystemResponse} for the given node name */
+  public NodeSystemResponse getNodeResponseForNode(String node) {
+    return nodesInfo.get(node);
+  }
+
+  public String getSolrImplVersion() {
+    return getNodeResponse() != null && getNodeResponse().lucene != null
+        ? getNodeResponse().lucene.solrImplVersion
+        : null;
+  }
+
+  public String getSolrSpecVersion() {
+    return getNodeResponse() != null && getNodeResponse().lucene != null
+        ? getNodeResponse().lucene.solrSpecVersion
+        : null;
+  }
+
+  public Date getJVMStartTime() {
+    return getNodeResponse() != null
+            && getNodeResponse().jvm != null
+            && getNodeResponse().jvm.jmx != null
+        ? getNodeResponse().jvm.jmx.startTime
+        : null;
+  }
+
+  public Long getJVMUpTimeMillis() {
+    return getNodeResponse() != null
+            && getNodeResponse().jvm != null
+            && getNodeResponse().jvm.jmx != null
+        ? getNodeResponse().jvm.jmx.upTimeMS
+        : null;
+  }
+
+  public String getHumanReadableJVMMemoryUsed() {
+    return getNodeResponse() != null
+            && getNodeResponse().jvm != null
+            && getNodeResponse().jvm.memory != null
+        ? getNodeResponse().jvm.memory.used
+        : null;
+  }
+
+  public String getHumanReadableJVMMemoryTotal() {
+    return getNodeResponse() != null
+            && getNodeResponse().jvm != null
+            && getNodeResponse().jvm.memory != null
+        ? getNodeResponse().jvm.memory.total
+        : null;
+  }
+}
diff --git 
a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java 
b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
index 052d0104bff..b26360864c5 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java
@@ -205,6 +205,7 @@ public interface CommonParams {
   String ZK_STATUS_PATH = "/admin/zookeeper/status";
   String SYSTEM_INFO_PATH = "/admin/info/system";
   String METRICS_PATH = "/admin/metrics";
+  String V2_SYSTEM_INFO_PATH = "/node/system";
 
   String STATUS = "status";
 
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java 
b/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
index 8935d0301a5..759dd72a153 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
@@ -441,7 +441,7 @@ public abstract class SolrExampleTests extends 
SolrExampleTestsBase {
       String url = getBaseUrl();
       try (SolrClient adminClient = getHttpSolrClient(url)) {
         SolrQuery q = new SolrQuery();
-        q.set("qt", "/admin/info/system");
+        q.set("qt", CommonParams.SYSTEM_INFO_PATH);
 
         QueryResponse rsp = adminClient.query(q);
         assertNotNull(rsp.getResponse().get("mode"));
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/response/SystemInfoResponseTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/response/SystemInfoResponseTest.java
new file mode 100644
index 00000000000..f129d63f10e
--- /dev/null
+++ 
b/solr/solrj/src/test/org/apache/solr/client/solrj/response/SystemInfoResponseTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.solr.client.solrj.response;
+
+import java.io.IOException;
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.SystemInfoRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.params.MapSolrParams;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class SystemInfoResponseTest extends SolrCloudTestCase {
+
+  private CloudSolrClient solrClient;
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(2).addConfig("config", 
getFile("solrj/solr/collection1/conf")).configure();
+  }
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    solrClient = cluster.getSolrClient();
+  }
+
+  @Test
+  public void testAllNodesResponse() throws SolrServerException, IOException {
+    MapSolrParams params = new MapSolrParams(Map.of("nodes", "all"));
+
+    SystemInfoRequest req = new SystemInfoRequest(params);
+    SystemInfoResponse rsp = req.process(solrClient);
+
+    try {
+      rsp.getNodeResponse();
+      Assert.fail("Should throw UnsupportedOperationException");
+    } catch (UnsupportedOperationException e) {
+      Assert.assertTrue(e.getMessage().startsWith("Multiple nodes system info 
available"));
+    }
+
+    Assert.assertEquals(2, rsp.getAllNodeResponses().size());
+    Assert.assertEquals(2, rsp.getAllCoreRoots().size());
+    Assert.assertEquals(2, rsp.getAllModes().size());
+  }
+
+  @Test
+  public void testResponseForGivenNode() throws SolrServerException, 
IOException {
+    MapSolrParams params = new MapSolrParams(Map.of("nodes", "all"));
+
+    SystemInfoRequest req = new SystemInfoRequest(params);
+    SystemInfoResponse rsp = req.process(solrClient);
+
+    for (String node : rsp.getAllNodes()) {
+      String coreRoot = rsp.getCoreRootForNode(node);
+      Assert.assertEquals(node, rsp.getNodeForCoreRoot(coreRoot));
+      String solrHome = rsp.getCoreRootForNode(node);
+      Assert.assertEquals(node, rsp.getNodeForSolrHome(solrHome));
+    }
+  }
+}
diff --git 
a/solr/test-framework/src/java/org/apache/solr/util/SolrJMetricTestUtils.java 
b/solr/test-framework/src/java/org/apache/solr/util/SolrJMetricTestUtils.java
index 556aee93650..1ec456202c6 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/util/SolrJMetricTestUtils.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/util/SolrJMetricTestUtils.java
@@ -21,10 +21,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.client.solrj.request.MetricsRequest;
 import org.apache.solr.client.solrj.response.InputStreamResponseParser;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
@@ -33,12 +32,7 @@ public final class SolrJMetricTestUtils {
 
   public static double getPrometheusMetricValue(SolrClient solrClient, String 
metricName)
       throws SolrServerException, IOException {
-    var req =
-        new GenericSolrRequest(
-            SolrRequest.METHOD.GET,
-            "/admin/metrics",
-            SolrRequest.SolrRequestType.ADMIN,
-            SolrParams.of("wt", "prometheus"));
+    var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
     req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
     NamedList<Object> resp = solrClient.request(req);
@@ -57,12 +51,7 @@ public final class SolrJMetricTestUtils {
       throws SolrServerException, IOException {
 
     try (var client = new HttpJettySolrClient.Builder(baseUrl).build()) {
-      var req =
-          new GenericSolrRequest(
-              SolrRequest.METHOD.GET,
-              "/admin/metrics",
-              SolrRequest.SolrRequestType.ADMIN,
-              SolrParams.of("wt", "prometheus"));
+      var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
       req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
       NamedList<Object> resp = client.request(req);
@@ -90,12 +79,7 @@ public final class SolrJMetricTestUtils {
       throws SolrServerException, IOException {
 
     try (var client = new HttpJettySolrClient.Builder(baseUrl).build()) {
-      var req =
-          new GenericSolrRequest(
-              SolrRequest.METHOD.GET,
-              "/admin/metrics",
-              SolrRequest.SolrRequestType.ADMIN,
-              SolrParams.of("wt", "prometheus"));
+      var req = new MetricsRequest(SolrParams.of("wt", "prometheus"));
       req.setResponseParser(new InputStreamResponseParser("prometheus"));
 
       NamedList<Object> resp = client.request(req);

Reply via email to