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

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


The following commit(s) were added to refs/heads/main by this push:
     new c74142675eb SOLR-16458: Convert /api/node/system to JAX-RS (#4078)
c74142675eb is described below

commit c74142675ebdabf0ebab8bf8b162223f63262af3
Author: igiguere <[email protected]>
AuthorDate: Mon Mar 16 13:51:28 2026 -0400

    SOLR-16458: Convert /api/node/system to JAX-RS (#4078)
    
    This migration to JAX-RS implicitly adds these APIs to the OAS, and ensures
    autogeneration of SolrRequest/SolrResponse types.
    
    Co-authored-by: Isabelle Giguere <[email protected]>
    Co-authored-by: Jason Gerlowski <[email protected]>
---
 ...078-node-system-response-v2-jersey-resource.yml |   8 +
 .../client/api/endpoint/NodeSystemInfoApi.java     |  34 ++
 .../solr/client/api/model/NodeSystemResponse.java  |  11 +-
 .../apache/solr/handler/admin/CoreInfoHandler.java |  39 +-
 .../solr/handler/admin/SystemInfoHandler.java      | 457 +---------------
 ...temInfoHandler.java => SystemInfoProvider.java} | 574 +++++++++++----------
 .../solr/handler/admin/api/GetNodeSystemInfo.java  |  81 +++
 .../solr/handler/admin/api/NodeSystemInfoAPI.java  |  50 --
 .../solr/packagemanager/RepositoryManager.java     |  15 +-
 .../handler/admin/NodeSystemInfoProviderTest.java  |  83 +++
 .../solr/handler/admin/SystemInfoHandlerTest.java  |  46 --
 .../handler/admin/api/GetNodeSystemInfoTest.java   |  58 +++
 .../handler/admin/api/V2NodeAPIMappingTest.java    |  20 -
 .../pages/system-info-handler.adoc                 | 391 +++++++-------
 .../client/solrj/request/SystemInfoRequest.java    |  19 +-
 .../client/solrj/response/SystemInfoResponse.java  |  33 +-
 .../json/JacksonDataBindResponseParser.java        |   3 +
 .../apache/solr/common/params/CommonParams.java    |   1 -
 .../solrj/response/SystemInfoResponseTest.java     |  34 +-
 19 files changed, 822 insertions(+), 1135 deletions(-)

diff --git 
a/changelog/unreleased/PR#4078-node-system-response-v2-jersey-resource.yml 
b/changelog/unreleased/PR#4078-node-system-response-v2-jersey-resource.yml
new file mode 100644
index 00000000000..6cbea3de784
--- /dev/null
+++ b/changelog/unreleased/PR#4078-node-system-response-v2-jersey-resource.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Add V2 "System Info" API and SolrJ request 'SystemApi.GetNodeSystemInfo'
+type: added # added, changed, fixed, deprecated, removed, dependency_update, 
security, other
+authors:
+  - name: Isabelle Giguère
+links:
+  - name: PR#4078
+    url: https://github.com/apache/solr/pull/4078
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeSystemInfoApi.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeSystemInfoApi.java
new file mode 100644
index 00000000000..07342668102
--- /dev/null
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeSystemInfoApi.java
@@ -0,0 +1,34 @@
+/*
+ * 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.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import org.apache.solr.client.api.model.NodeSystemResponse;
+
+/** V2 API definitions to fetch node system info, analogous to the v1 
/admin/info/system. */
+@Path("/node/system")
+public interface NodeSystemInfoApi {
+
+  @GET
+  @Operation(
+      summary = "Retrieve all node system info.",
+      tags = {"system"})
+  NodeSystemResponse getNodeSystemInfo(@QueryParam(value = "nodes") String 
nodes);
+}
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
index efd73c269dc..024f53cdffc 100644
--- 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
@@ -20,12 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** Response from /node/system */
 public class NodeSystemResponse extends SolrJerseyResponse {
 
-  @JsonProperty public String mode;
   @JsonProperty public String host;
+  @JsonProperty public String node;
+  @JsonProperty public String mode;
   @JsonProperty public String zkHost;
 
   @JsonProperty("solr_home")
@@ -42,7 +44,6 @@ public class NodeSystemResponse extends SolrJerseyResponse {
   @JsonProperty(value = "environment_color")
   public String environmentColor;
 
-  @JsonProperty public String node;
   @JsonProperty public Lucene lucene;
   @JsonProperty public JVM jvm;
   @JsonProperty public Security security;
@@ -55,8 +56,8 @@ public class NodeSystemResponse extends SolrJerseyResponse {
     @JsonProperty public String authenticationPlugin;
     @JsonProperty public String authorizationPlugin;
     @JsonProperty public String username;
-    @JsonProperty public List<String> roles;
-    @JsonProperty public List<String> permissions;
+    @JsonProperty public Set<String> roles;
+    @JsonProperty public Set<String> permissions;
   }
 
   /** /node/system/lucene */
@@ -122,6 +123,6 @@ public class NodeSystemResponse extends SolrJerseyResponse {
     @JsonProperty public boolean available;
     @JsonProperty public long count;
     @JsonProperty public MemoryRaw memory;
-    @JsonProperty Map<String, Object> devices;
+    @JsonProperty public Map<String, Object> devices;
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/CoreInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/CoreInfoHandler.java
index 4433713209d..2279fb03b17 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreInfoHandler.java
@@ -16,16 +16,10 @@
  */
 package org.apache.solr.handler.admin;
 
-import java.io.IOException;
 import java.lang.invoke.MethodHandles;
-import java.nio.file.Path;
-import java.util.Date;
-import org.apache.solr.common.util.SimpleOrderedMap;
-import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.security.AuthorizationContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,38 +48,7 @@ public class CoreInfoHandler extends RequestHandlerBase {
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     rsp.setHttpCaching(false);
-    SolrCore core = req.getCore();
 
-    rsp.add("core", getCoreInfo(core, req.getSchema()));
-  }
-
-  private SimpleOrderedMap<Object> getCoreInfo(SolrCore core, IndexSchema 
schema) {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-
-    info.add("schema", schema != null ? schema.getSchemaName() : "no schema!");
-
-    // Now
-    info.add("now", new Date());
-
-    // Start Time
-    info.add("start", core.getStartTimeStamp());
-
-    // Solr Home
-    SimpleOrderedMap<Object> dirs = new SimpleOrderedMap<>();
-    dirs.add("cwd", 
Path.of(System.getProperty("user.dir")).toAbsolutePath().toString());
-    dirs.add("instance", core.getInstancePath().toString());
-    try {
-      dirs.add("data", 
core.getDirectoryFactory().normalize(core.getDataDir()));
-    } catch (IOException e) {
-      log.warn("Problem getting the normalized data directory path", e);
-    }
-    dirs.add("dirimpl", core.getDirectoryFactory().getClass().getName());
-    try {
-      dirs.add("index", 
core.getDirectoryFactory().normalize(core.getIndexDir()));
-    } catch (IOException e) {
-      log.warn("Problem getting the normalized index directory path", e);
-    }
-    info.add("directory", dirs);
-    return info;
+    rsp.add("core", SystemInfoProvider.getCoreInfo(req.getCore(), 
req.getSchema()));
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
index eb0079b5080..20fe09098d2 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
@@ -16,52 +16,18 @@
  */
 package org.apache.solr.handler.admin;
 
-import static org.apache.solr.common.params.CommonParams.NAME;
-
-import java.beans.BeanInfo;
-import java.beans.IntrospectionException;
-import java.beans.Introspector;
-import java.beans.PropertyDescriptor;
 import java.lang.invoke.MethodHandles;
-import java.lang.management.ManagementFactory;
-import java.lang.management.OperatingSystemMXBean;
-import java.lang.management.PlatformManagedObject;
-import java.lang.management.RuntimeMXBean;
-import java.lang.reflect.Method;
-import java.net.InetAddress;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.function.BiConsumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.lucene.util.Version;
-import org.apache.solr.api.AnnotatedApi;
-import org.apache.solr.api.Api;
-import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.util.EnvUtils;
-import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.model.NodeSystemResponse;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.NodeConfig;
-import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.handler.admin.api.NodeSystemInfoAPI;
-import org.apache.solr.metrics.GpuMetricsProvider;
+import org.apache.solr.handler.admin.api.GetNodeSystemInfo;
+import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
-import org.apache.solr.security.AuthorizationPlugin;
-import org.apache.solr.security.PKIAuthenticationPlugin;
-import org.apache.solr.security.RuleBasedAuthorizationPluginBase;
-import org.apache.solr.util.RTimer;
-import org.apache.solr.util.stats.MetricUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -72,182 +38,24 @@ import org.slf4j.LoggerFactory;
 public class SystemInfoHandler extends RequestHandlerBase {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  /**
-   * Expert level system property to prevent doing a reverse lookup of our 
hostname. This property
-   * will be logged as a suggested workaround if any problems are noticed when 
doing reverse lookup.
-   *
-   * <p>TODO: should we refactor this (and the associated logic) into a helper 
method for any other
-   * places where DNS is used?
-   *
-   * @see #initHostname
-   */
-  private static final String REVERSE_DNS_OF_LOCALHOST_SYSPROP =
-      "solr.admin.handler.systeminfo.dns.reverse.lookup.enabled";
-
-  /**
-   * Local cache for BeanInfo instances that are created to scan for system 
metrics. List of
-   * properties is not supposed to change for the JVM lifespan, so we can keep 
already create
-   * BeanInfo instance for future calls.
-   */
-  private static final ConcurrentMap<Class<?>, BeanInfo> beanInfos = new 
ConcurrentHashMap<>();
-
-  // on some platforms, resolving canonical hostname can cause the thread
-  // to block for several seconds if name services aren't available
-  // so resolve this once per handler instance
-  // (ie: not static, so core reload will refresh)
-  private String hostname = null;
-
   private CoreContainer cc;
 
   public SystemInfoHandler(CoreContainer cc) {
     super();
     this.cc = cc;
-    initHostname();
-  }
-
-  /**
-   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
-   * property name and its current value.
-   *
-   * @param obj an instance of MXBean
-   * @param interfaces interfaces that it may implement. Each interface will 
be tried in turn, and
-   *     only if it exists and if it contains unique properties then they will 
be added as metrics.
-   * @param consumer consumer for each property name and value
-   * @param <T> formal type
-   */
-  public static <T extends PlatformManagedObject> void forEachGetterValue(
-      T obj, String[] interfaces, BiConsumer<String, Object> consumer) {
-    for (String clazz : interfaces) {
-      try {
-        final Class<? extends PlatformManagedObject> intf =
-            Class.forName(clazz).asSubclass(PlatformManagedObject.class);
-        forEachGetterValue(obj, intf, consumer);
-      } catch (ClassNotFoundException e) {
-        // ignore
-      }
-    }
-  }
-
-  /**
-   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
-   * property name and its current value.
-   *
-   * @param obj an instance of MXBean
-   * @param intf MXBean interface, one of {@link PlatformManagedObject}-s
-   * @param consumer consumer for each property name and value
-   * @param <T> formal type
-   */
-  public static <T extends PlatformManagedObject> void forEachGetterValue(
-      T obj, Class<? extends T> intf, BiConsumer<String, Object> consumer) {
-    if (intf.isInstance(obj)) {
-      BeanInfo beanInfo =
-          beanInfos.computeIfAbsent(
-              intf,
-              clazz -> {
-                try {
-                  return Introspector.getBeanInfo(
-                      clazz, clazz.getSuperclass(), 
Introspector.IGNORE_ALL_BEANINFO);
-
-                } catch (IntrospectionException e) {
-                  log.warn("Unable to fetch properties of MXBean {}", 
obj.getClass().getName());
-                  return null;
-                }
-              });
-
-      // if BeanInfo retrieval failed, return early
-      if (beanInfo == null) {
-        return;
-      }
-      for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
-        try {
-          Method readMethod = desc.getReadMethod();
-          if (readMethod == null) {
-            continue; // skip properties without a read method
-          }
-
-          final String name = desc.getName();
-          Object value = readMethod.invoke(obj);
-          consumer.accept(name, value);
-        } catch (Exception e) {
-          // didn't work, skip it...
-        }
-      }
-    }
-  }
-
-  private void initHostname() {
-    if (!EnvUtils.getPropertyAsBool(REVERSE_DNS_OF_LOCALHOST_SYSPROP, true)) {
-      log.info(
-          "Resolving canonical hostname for local host prevented due to '{}' 
sysprop",
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP);
-      hostname = null;
-      return;
-    }
-
-    RTimer timer = new RTimer();
-    try {
-      InetAddress addr = InetAddress.getLocalHost();
-      hostname = addr.getCanonicalHostName();
-    } catch (Exception e) {
-      log.warn(
-          "Unable to resolve canonical hostname for local host, possible DNS 
misconfiguration. Set the '{}' sysprop to false on startup to prevent future 
lookups if DNS can not be fixed.",
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP,
-          e);
-      hostname = null;
-      return;
-    }
-    timer.stop();
-
-    if (15000D < timer.getTime()) {
-      String readableTime = String.format(Locale.ROOT, "%.3f", 
(timer.getTime() / 1000));
-      log.warn(
-          "Resolving canonical hostname for local host took {} seconds, 
possible DNS misconfiguration. Set the '{}' sysprop to false on startup to 
prevent future lookups if DNS can not be fixed.",
-          readableTime,
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP);
-    }
   }
 
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     rsp.setHttpCaching(false);
+
     if (AdminHandlersProxy.maybeProxyToNodes(req, rsp, getCoreContainer(req))) 
{
       return; // Request was proxied to other node
     }
-    boolean solrCloudMode = getCoreContainer(req).isZooKeeperAware();
-    rsp.add("mode", solrCloudMode ? "solrcloud" : "std");
 
-    rsp.add("host", hostname);
-
-    if (solrCloudMode) {
-      rsp.add("zkHost", 
getCoreContainer(req).getZkController().getZkServerAddress());
-    }
-    if (cc != null) {
-      rsp.add("solr_home", cc.getSolrHome());
-      rsp.add("core_root", cc.getCoreRootDirectory());
-    }
-
-    rsp.add("lucene", getLuceneInfo());
-    NodeConfig nodeConfig = getCoreContainer(req).getNodeConfig();
-    rsp.add("jvm", getJvmInfo(nodeConfig));
-    rsp.add("security", getSecurityInfo(req));
-    rsp.add("system", getSystemInfo());
-
-    rsp.add("gpu", getGpuInfo(req));
-    if (solrCloudMode) {
-      rsp.add("node", getCoreContainer(req).getZkController().getNodeName());
-    }
-    SolrEnvironment env =
-        SolrEnvironment.getFromSyspropOrClusterprop(
-            solrCloudMode ? 
getCoreContainer(req).getZkController().zkStateReader : null);
-    if (env.isDefined()) {
-      rsp.add("environment", env.getCode());
-      if (env.getLabel() != null) {
-        rsp.add("environment_label", env.getLabel());
-      }
-      if (env.getColor() != null) {
-        rsp.add("environment_color", env.getColor());
-      }
-    }
+    SystemInfoProvider provider = new SystemInfoProvider(req);
+    NodeSystemResponse response = provider.getNodeSystemInfo(new 
NodeSystemResponse());
+    V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
   }
 
   private CoreContainer getCoreContainer(SolrQueryRequest req) {
@@ -255,167 +63,6 @@ public class SystemInfoHandler extends RequestHandlerBase {
     return coreContainer == null ? cc : coreContainer;
   }
 
-  /** Get system info */
-  public static SimpleOrderedMap<Object> getSystemInfo() {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-
-    OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
-    info.add(NAME, os.getName()); // add at least this one
-
-    // add remaining ones dynamically using Java Beans API
-    // also those from JVM implementation-specific classes
-    forEachGetterValue(
-        os,
-        MetricUtils.OS_MXBEAN_CLASSES,
-        (name, value) -> {
-          if (info.get(name) == null) {
-            info.add(name, value);
-          }
-        });
-
-    return info;
-  }
-
-  /** Get JVM Info - including memory info */
-  public static SimpleOrderedMap<Object> getJvmInfo(NodeConfig nodeConfig) {
-    SimpleOrderedMap<Object> jvm = new SimpleOrderedMap<>();
-
-    final String javaVersion = 
System.getProperty("java.specification.version", "unknown");
-    final String javaVendor = System.getProperty("java.specification.vendor", 
"unknown");
-    final String javaName = System.getProperty("java.specification.name", 
"unknown");
-    final String jreVersion = System.getProperty("java.version", "unknown");
-    final String jreVendor = System.getProperty("java.vendor", "unknown");
-    final String vmVersion = System.getProperty("java.vm.version", "unknown");
-    final String vmVendor = System.getProperty("java.vm.vendor", "unknown");
-    final String vmName = System.getProperty("java.vm.name", "unknown");
-
-    // Summary Info
-    jvm.add("version", jreVersion + " " + vmVersion);
-    jvm.add(NAME, jreVendor + " " + vmName);
-
-    // details
-    SimpleOrderedMap<Object> java = new SimpleOrderedMap<>();
-    java.add("vendor", javaVendor);
-    java.add(NAME, javaName);
-    java.add("version", javaVersion);
-    jvm.add("spec", java);
-    SimpleOrderedMap<Object> jre = new SimpleOrderedMap<>();
-    jre.add("vendor", jreVendor);
-    jre.add("version", jreVersion);
-    jvm.add("jre", jre);
-    SimpleOrderedMap<Object> vm = new SimpleOrderedMap<>();
-    vm.add("vendor", vmVendor);
-    vm.add(NAME, vmName);
-    vm.add("version", vmVersion);
-    jvm.add("vm", vm);
-
-    Runtime runtime = Runtime.getRuntime();
-    jvm.add("processors", runtime.availableProcessors());
-
-    // not thread safe, but could be thread local
-    DecimalFormat df = new DecimalFormat("#.#", 
DecimalFormatSymbols.getInstance(Locale.ROOT));
-
-    SimpleOrderedMap<Object> mem = new SimpleOrderedMap<>();
-    SimpleOrderedMap<Object> raw = new SimpleOrderedMap<>();
-    long free = runtime.freeMemory();
-    long max = runtime.maxMemory();
-    long total = runtime.totalMemory();
-    long used = total - free;
-    double percentUsed = ((double) (used) / (double) max) * 100;
-    raw.add("free", free);
-    mem.add("free", humanReadableUnits(free, df));
-    raw.add("total", total);
-    mem.add("total", humanReadableUnits(total, df));
-    raw.add("max", max);
-    mem.add("max", humanReadableUnits(max, df));
-    raw.add("used", used);
-    mem.add("used", humanReadableUnits(used, df) + " (%" + 
df.format(percentUsed) + ")");
-    raw.add("used%", percentUsed);
-
-    mem.add("raw", raw);
-    jvm.add("memory", mem);
-
-    // JMX properties -- probably should be moved to a different handler
-    SimpleOrderedMap<Object> jmx = new SimpleOrderedMap<>();
-    try {
-      RuntimeMXBean mx = ManagementFactory.getRuntimeMXBean();
-      if (mx.isBootClassPathSupported()) {
-        jmx.add("bootclasspath", mx.getBootClassPath());
-      }
-      jmx.add("classpath", mx.getClassPath());
-
-      // the input arguments passed to the Java virtual machine
-      // which does not include the arguments to the main method.
-      jmx.add("commandLineArgs", getInputArgumentsRedacted(nodeConfig, mx));
-
-      jmx.add("startTime", new Date(mx.getStartTime()));
-      jmx.add("upTimeMS", mx.getUptime());
-
-    } catch (Exception e) {
-      log.warn("Error getting JMX properties", e);
-    }
-    jvm.add("jmx", jmx);
-    return jvm;
-  }
-
-  /** Get Security Info */
-  public SimpleOrderedMap<Object> getSecurityInfo(SolrQueryRequest req) {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-
-    if (cc != null) {
-      if (cc.getAuthenticationPlugin() != null) {
-        info.add("authenticationPlugin", 
cc.getAuthenticationPlugin().getName());
-      }
-      if (cc.getAuthorizationPlugin() != null) {
-        info.add("authorizationPlugin", 
cc.getAuthorizationPlugin().getClass().getName());
-      }
-    }
-
-    if (req.getUserPrincipal() != null
-        && req.getUserPrincipal() != 
PKIAuthenticationPlugin.CLUSTER_MEMBER_NODE) {
-      // User principal
-      info.add("username", req.getUserPrincipal().getName());
-
-      // Mapped roles for this principal
-      @SuppressWarnings("resource")
-      AuthorizationPlugin auth = cc == null ? null : 
cc.getAuthorizationPlugin();
-      if (auth instanceof RuleBasedAuthorizationPluginBase rbap) {
-        Set<String> roles = rbap.getUserRoles(req.getUserPrincipal());
-        info.add("roles", roles);
-        if (roles == null) {
-          info.add("permissions", Set.of());
-        } else {
-          info.add(
-              "permissions",
-              rbap.getPermissionNamesForRoles(
-                  Stream.concat(roles.stream(), Stream.of("*", 
null)).collect(Collectors.toSet())));
-        }
-      }
-    }
-
-    if (cc != null && cc.getZkController() != null) {
-      String urlScheme =
-          
cc.getZkController().zkStateReader.getClusterProperty(ZkStateReader.URL_SCHEME, 
"http");
-      info.add("tls", ZkStateReader.HTTPS.equals(urlScheme));
-    }
-
-    return info;
-  }
-
-  private static SimpleOrderedMap<Object> getLuceneInfo() {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-
-    Package p = SolrCore.class.getPackage();
-
-    info.add("solr-spec-version", p.getSpecificationVersion());
-    info.add("solr-impl-version", p.getImplementationVersion());
-
-    info.add("lucene-spec-version", Version.LATEST.toString());
-    info.add("lucene-impl-version", Version.getPackageImplementationVersion());
-
-    return info;
-  }
-
   //////////////////////// SolrInfoMBeans methods //////////////////////
 
   @Override
@@ -428,49 +75,9 @@ public class SystemInfoHandler extends RequestHandlerBase {
     return Category.ADMIN;
   }
 
-  private static final long ONE_KB = 1024;
-  private static final long ONE_MB = ONE_KB * ONE_KB;
-  private static final long ONE_GB = ONE_KB * ONE_MB;
-
-  /** Return good default units based on byte size. */
-  private static String humanReadableUnits(long bytes, DecimalFormat df) {
-    String newSizeAndUnits;
-
-    if (bytes / ONE_GB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_GB) + " GB";
-    } else if (bytes / ONE_MB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_MB) + " MB";
-    } else if (bytes / ONE_KB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_KB) + " KB";
-    } else {
-      newSizeAndUnits = bytes + " bytes";
-    }
-
-    return newSizeAndUnits;
-  }
-
-  private static List<String> getInputArgumentsRedacted(NodeConfig nodeConfig, 
RuntimeMXBean mx) {
-    List<String> list = new ArrayList<>();
-    for (String arg : mx.getInputArguments()) {
-      if (arg.startsWith("-D")
-          && arg.contains("=")
-          && nodeConfig.isSysPropHidden(arg.substring(2, arg.indexOf('=')))) {
-        list.add(
-            String.format(
-                Locale.ROOT,
-                "%s=%s",
-                arg.substring(0, arg.indexOf('=')),
-                NodeConfig.REDACTED_SYS_PROP_VALUE));
-      } else {
-        list.add(arg);
-      }
-    }
-    return list;
-  }
-
   @Override
-  public Collection<Api> getApis() {
-    return AnnotatedApi.getApis(new NodeSystemInfoAPI(this));
+  public Collection<Class<? extends JerseyResource>> getJerseyResources() {
+    return Set.of(GetNodeSystemInfo.class);
   }
 
   @Override
@@ -478,50 +85,6 @@ public class SystemInfoHandler extends RequestHandlerBase {
     return Boolean.TRUE;
   }
 
-  private SimpleOrderedMap<Object> getGpuInfo(SolrQueryRequest req) {
-    SimpleOrderedMap<Object> gpuInfo = new SimpleOrderedMap<>();
-
-    try {
-      GpuMetricsProvider provider = 
getCoreContainer(req).getGpuMetricsProvider();
-
-      if (provider == null) {
-        gpuInfo.add("available", false);
-        return gpuInfo;
-      }
-
-      long gpuCount = provider.getGpuCount();
-      if (gpuCount > 0) {
-        gpuInfo.add("available", true);
-        gpuInfo.add("count", gpuCount);
-
-        long gpuMemoryTotal = provider.getGpuMemoryTotal();
-        long gpuMemoryUsed = provider.getGpuMemoryUsed();
-        long gpuMemoryFree = provider.getGpuMemoryFree();
-
-        if (gpuMemoryTotal > 0) {
-          SimpleOrderedMap<Object> memory = new SimpleOrderedMap<>();
-          memory.add("total", gpuMemoryTotal);
-          memory.add("used", gpuMemoryUsed);
-          memory.add("free", gpuMemoryFree);
-          gpuInfo.add("memory", memory);
-        }
-
-        var devices = provider.getGpuDevices();
-        if (devices != null && devices.size() > 0) {
-          gpuInfo.add("devices", devices);
-        }
-      } else {
-        gpuInfo.add("available", false);
-      }
-
-    } catch (Exception e) {
-      log.warn("Failed to get GPU information", e);
-      gpuInfo.add("available", false);
-    }
-
-    return gpuInfo;
-  }
-
   @Override
   public Name getPermissionName(AuthorizationContext request) {
     return Name.CONFIG_READ_PERM;
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoProvider.java
similarity index 58%
copy from 
solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
copy to solr/core/src/java/org/apache/solr/handler/admin/SystemInfoProvider.java
index eb0079b5080..03e7df5c4b0 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoProvider.java
@@ -22,6 +22,7 @@ import java.beans.BeanInfo;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
+import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
@@ -29,13 +30,15 @@ import java.lang.management.PlatformManagedObject;
 import java.lang.management.RuntimeMXBean;
 import java.lang.reflect.Method;
 import java.net.InetAddress;
+import java.nio.file.Path;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -43,20 +46,18 @@ import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.lucene.util.Version;
-import org.apache.solr.api.AnnotatedApi;
-import org.apache.solr.api.Api;
+import org.apache.solr.client.api.model.NodeSystemResponse;
+import org.apache.solr.client.api.util.SolrVersion;
 import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.NodeConfig;
 import org.apache.solr.core.SolrCore;
-import org.apache.solr.handler.RequestHandlerBase;
-import org.apache.solr.handler.admin.api.NodeSystemInfoAPI;
 import org.apache.solr.metrics.GpuMetricsProvider;
 import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.AuthorizationContext;
+import org.apache.solr.schema.IndexSchema;
 import org.apache.solr.security.AuthorizationPlugin;
 import org.apache.solr.security.PKIAuthenticationPlugin;
 import org.apache.solr.security.RuleBasedAuthorizationPluginBase;
@@ -65,16 +66,28 @@ import org.apache.solr.util.stats.MetricUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/**
- * This handler returns node/container level info. See {@link
- * org.apache.solr.handler.admin.CoreInfoHandler}
- */
-public class SystemInfoHandler extends RequestHandlerBase {
+/** Used by GetNodeSystemInfo, and indirectly by SystemInfoHandler */
+public class SystemInfoProvider {
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+  private SolrQueryRequest req;
+  private SolrParams params;
+  private CoreContainer cc;
+
+  // on some platforms, resolving canonical hostname can cause the thread
+  // to block for several seconds if nameservices aren't available
+  // so resolve this once per provider instance
+  // (ie: not static, so core reload will refresh)
+  private String hostname;
+
+  private static final long ONE_KB = 1024;
+  private static final long ONE_MB = ONE_KB * ONE_KB;
+  private static final long ONE_GB = ONE_KB * ONE_MB;
+
   /**
-   * Expert level system property to prevent doing a reverse lookup of our 
hostname. This property
-   * will be logged as a suggested workaround if any problems are noticed when 
doing reverse lookup.
+   * Undocumented expert level system property to prevent doing a reverse 
lookup of our hostname.
+   * This property will be logged as a suggested workaround if any problems 
are noticed when doing
+   * reverse lookup.
    *
    * <p>TODO: should we refactor this (and the associated logic) into a helper 
method for any other
    * places where DNS is used?
@@ -91,176 +104,87 @@ public class SystemInfoHandler extends RequestHandlerBase 
{
    */
   private static final ConcurrentMap<Class<?>, BeanInfo> beanInfos = new 
ConcurrentHashMap<>();
 
-  // on some platforms, resolving canonical hostname can cause the thread
-  // to block for several seconds if name services aren't available
-  // so resolve this once per handler instance
-  // (ie: not static, so core reload will refresh)
-  private String hostname = null;
-
-  private CoreContainer cc;
-
-  public SystemInfoHandler(CoreContainer cc) {
-    super();
-    this.cc = cc;
+  public SystemInfoProvider(SolrQueryRequest request) {
+    req = request;
+    params = request.getParams();
+    cc = request.getCoreContainer();
     initHostname();
   }
 
-  /**
-   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
-   * property name and its current value.
-   *
-   * @param obj an instance of MXBean
-   * @param interfaces interfaces that it may implement. Each interface will 
be tried in turn, and
-   *     only if it exists and if it contains unique properties then they will 
be added as metrics.
-   * @param consumer consumer for each property name and value
-   * @param <T> formal type
-   */
-  public static <T extends PlatformManagedObject> void forEachGetterValue(
-      T obj, String[] interfaces, BiConsumer<String, Object> consumer) {
-    for (String clazz : interfaces) {
-      try {
-        final Class<? extends PlatformManagedObject> intf =
-            Class.forName(clazz).asSubclass(PlatformManagedObject.class);
-        forEachGetterValue(obj, intf, consumer);
-      } catch (ClassNotFoundException e) {
-        // ignore
-      }
-    }
-  }
-
-  /**
-   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
-   * property name and its current value.
-   *
-   * @param obj an instance of MXBean
-   * @param intf MXBean interface, one of {@link PlatformManagedObject}-s
-   * @param consumer consumer for each property name and value
-   * @param <T> formal type
-   */
-  public static <T extends PlatformManagedObject> void forEachGetterValue(
-      T obj, Class<? extends T> intf, BiConsumer<String, Object> consumer) {
-    if (intf.isInstance(obj)) {
-      BeanInfo beanInfo =
-          beanInfos.computeIfAbsent(
-              intf,
-              clazz -> {
-                try {
-                  return Introspector.getBeanInfo(
-                      clazz, clazz.getSuperclass(), 
Introspector.IGNORE_ALL_BEANINFO);
-
-                } catch (IntrospectionException e) {
-                  log.warn("Unable to fetch properties of MXBean {}", 
obj.getClass().getName());
-                  return null;
-                }
-              });
-
-      // if BeanInfo retrieval failed, return early
-      if (beanInfo == null) {
-        return;
-      }
-      for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
-        try {
-          Method readMethod = desc.getReadMethod();
-          if (readMethod == null) {
-            continue; // skip properties without a read method
-          }
-
-          final String name = desc.getName();
-          Object value = readMethod.invoke(obj);
-          consumer.accept(name, value);
-        } catch (Exception e) {
-          // didn't work, skip it...
-        }
-      }
-    }
-  }
-
-  private void initHostname() {
-    if (!EnvUtils.getPropertyAsBool(REVERSE_DNS_OF_LOCALHOST_SYSPROP, true)) {
-      log.info(
-          "Resolving canonical hostname for local host prevented due to '{}' 
sysprop",
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP);
-      hostname = null;
-      return;
-    }
-
-    RTimer timer = new RTimer();
-    try {
-      InetAddress addr = InetAddress.getLocalHost();
-      hostname = addr.getCanonicalHostName();
-    } catch (Exception e) {
-      log.warn(
-          "Unable to resolve canonical hostname for local host, possible DNS 
misconfiguration. Set the '{}' sysprop to false on startup to prevent future 
lookups if DNS can not be fixed.",
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP,
-          e);
-      hostname = null;
-      return;
-    }
-    timer.stop();
-
-    if (15000D < timer.getTime()) {
-      String readableTime = String.format(Locale.ROOT, "%.3f", 
(timer.getTime() / 1000));
-      log.warn(
-          "Resolving canonical hostname for local host took {} seconds, 
possible DNS misconfiguration. Set the '{}' sysprop to false on startup to 
prevent future lookups if DNS can not be fixed.",
-          readableTime,
-          REVERSE_DNS_OF_LOCALHOST_SYSPROP);
+  /** Fill-out the provided response with all system info. */
+  public NodeSystemResponse getNodeSystemInfo(NodeSystemResponse response) {
+    if (cc != null) {
+      response.solrHome = cc.getSolrHome().toString();
+      response.coreRoot = cc.getCoreRootDirectory().toString();
     }
-  }
 
-  @Override
-  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    rsp.setHttpCaching(false);
-    if (AdminHandlersProxy.maybeProxyToNodes(req, rsp, getCoreContainer(req))) 
{
-      return; // Request was proxied to other node
+    boolean solrCloudMode = cc != null && cc.isZooKeeperAware();
+    response.mode = solrCloudMode ? "solrcloud" : "std";
+    if (solrCloudMode) {
+      response.zkHost = cc.getZkController().getZkServerAddress();
+      response.node = cc.getZkController().getNodeName();
     }
-    boolean solrCloudMode = getCoreContainer(req).isZooKeeperAware();
-    rsp.add("mode", solrCloudMode ? "solrcloud" : "std");
 
-    rsp.add("host", hostname);
+    response.lucene = getLuceneInfo();
 
-    if (solrCloudMode) {
-      rsp.add("zkHost", 
getCoreContainer(req).getZkController().getZkServerAddress());
-    }
-    if (cc != null) {
-      rsp.add("solr_home", cc.getSolrHome());
-      rsp.add("core_root", cc.getCoreRootDirectory());
-    }
+    response.jvm = getJvmInfo();
 
-    rsp.add("lucene", getLuceneInfo());
-    NodeConfig nodeConfig = getCoreContainer(req).getNodeConfig();
-    rsp.add("jvm", getJvmInfo(nodeConfig));
-    rsp.add("security", getSecurityInfo(req));
-    rsp.add("system", getSystemInfo());
+    response.security = getSecurityInfo();
+    response.system = getSystemInfo();
+    response.gpu = getGpuInfo();
 
-    rsp.add("gpu", getGpuInfo(req));
-    if (solrCloudMode) {
-      rsp.add("node", getCoreContainer(req).getZkController().getNodeName());
-    }
     SolrEnvironment env =
         SolrEnvironment.getFromSyspropOrClusterprop(
-            solrCloudMode ? 
getCoreContainer(req).getZkController().zkStateReader : null);
+            solrCloudMode ? cc.getZkController().zkStateReader : null);
     if (env.isDefined()) {
-      rsp.add("environment", env.getCode());
+      response.environment = env.getCode();
       if (env.getLabel() != null) {
-        rsp.add("environment_label", env.getLabel());
+        response.environmentLabel = env.getLabel();
       }
       if (env.getColor() != null) {
-        rsp.add("environment_color", env.getColor());
+        response.environmentColor = env.getColor();
       }
     }
+    return response;
   }
 
-  private CoreContainer getCoreContainer(SolrQueryRequest req) {
-    CoreContainer coreContainer = req.getCoreContainer();
-    return coreContainer == null ? cc : coreContainer;
+  /** Get core Info for V1 */
+  public static SimpleOrderedMap<Object> getCoreInfo(SolrCore core, 
IndexSchema schema) {
+    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
+
+    info.add("schema", schema != null ? schema.getSchemaName() : "no schema!");
+
+    // Now
+    info.add("now", new Date());
+
+    // Start Time
+    info.add("start", core.getStartTimeStamp());
+
+    // Solr Home
+    SimpleOrderedMap<Object> dirs = new SimpleOrderedMap<>();
+    dirs.add("cwd", 
Path.of(System.getProperty("user.dir")).toAbsolutePath().toString());
+    dirs.add("instance", core.getInstancePath().toString());
+    try {
+      dirs.add("data", 
core.getDirectoryFactory().normalize(core.getDataDir()));
+    } catch (IOException e) {
+      log.warn("Problem getting the normalized data directory path", e);
+    }
+    dirs.add("dirimpl", core.getDirectoryFactory().getClass().getName());
+    try {
+      dirs.add("index", 
core.getDirectoryFactory().normalize(core.getIndexDir()));
+    } catch (IOException e) {
+      log.warn("Problem getting the normalized index directory path", e);
+    }
+    info.add("directory", dirs);
+    return info;
   }
 
   /** Get system info */
-  public static SimpleOrderedMap<Object> getSystemInfo() {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
+  public Map<String, String> getSystemInfo() {
+    Map<String, String> info = new HashMap<>();
 
     OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
-    info.add(NAME, os.getName()); // add at least this one
+    info.put(NAME, os.getName()); // add at least this one
 
     // add remaining ones dynamically using Java Beans API
     // also those from JVM implementation-specific classes
@@ -269,7 +193,7 @@ public class SystemInfoHandler extends RequestHandlerBase {
         MetricUtils.OS_MXBEAN_CLASSES,
         (name, value) -> {
           if (info.get(name) == null) {
-            info.add(name, value);
+            info.put(name, String.valueOf(value));
           }
         });
 
@@ -277,8 +201,8 @@ public class SystemInfoHandler extends RequestHandlerBase {
   }
 
   /** Get JVM Info - including memory info */
-  public static SimpleOrderedMap<Object> getJvmInfo(NodeConfig nodeConfig) {
-    SimpleOrderedMap<Object> jvm = new SimpleOrderedMap<>();
+  public NodeSystemResponse.JVM getJvmInfo() {
+    NodeSystemResponse.JVM jvm = new NodeSystemResponse.JVM();
 
     final String javaVersion = 
System.getProperty("java.specification.version", "unknown");
     final String javaVendor = System.getProperty("java.specification.vendor", 
"unknown");
@@ -290,105 +214,107 @@ public class SystemInfoHandler extends 
RequestHandlerBase {
     final String vmName = System.getProperty("java.vm.name", "unknown");
 
     // Summary Info
-    jvm.add("version", jreVersion + " " + vmVersion);
-    jvm.add(NAME, jreVendor + " " + vmName);
+    jvm.version = jreVersion + " " + vmVersion;
+    jvm.name = jreVendor + " " + vmName;
 
     // details
-    SimpleOrderedMap<Object> java = new SimpleOrderedMap<>();
-    java.add("vendor", javaVendor);
-    java.add(NAME, javaName);
-    java.add("version", javaVersion);
-    jvm.add("spec", java);
-    SimpleOrderedMap<Object> jre = new SimpleOrderedMap<>();
-    jre.add("vendor", jreVendor);
-    jre.add("version", jreVersion);
-    jvm.add("jre", jre);
-    SimpleOrderedMap<Object> vm = new SimpleOrderedMap<>();
-    vm.add("vendor", vmVendor);
-    vm.add(NAME, vmName);
-    vm.add("version", vmVersion);
-    jvm.add("vm", vm);
+    NodeSystemResponse.Vendor spec = new NodeSystemResponse.Vendor();
+    spec.vendor = javaVendor;
+    spec.name = javaName;
+    spec.version = javaVersion;
+    jvm.spec = spec;
+
+    NodeSystemResponse.Vendor jre = new NodeSystemResponse.Vendor();
+    jre.vendor = jreVendor;
+    jre.version = jreVersion;
+    jvm.jre = jre;
+
+    NodeSystemResponse.Vendor vm = new NodeSystemResponse.Vendor();
+    vm.vendor = vmVendor;
+    vm.name = vmName;
+    vm.version = vmVersion;
+    jvm.vm = vm;
 
     Runtime runtime = Runtime.getRuntime();
-    jvm.add("processors", runtime.availableProcessors());
+    jvm.processors = runtime.availableProcessors();
 
     // not thread safe, but could be thread local
     DecimalFormat df = new DecimalFormat("#.#", 
DecimalFormatSymbols.getInstance(Locale.ROOT));
 
-    SimpleOrderedMap<Object> mem = new SimpleOrderedMap<>();
-    SimpleOrderedMap<Object> raw = new SimpleOrderedMap<>();
+    NodeSystemResponse.JvmMemory mem = new NodeSystemResponse.JvmMemory();
+    NodeSystemResponse.JvmMemoryRaw raw = new 
NodeSystemResponse.JvmMemoryRaw();
     long free = runtime.freeMemory();
     long max = runtime.maxMemory();
     long total = runtime.totalMemory();
     long used = total - free;
     double percentUsed = ((double) (used) / (double) max) * 100;
-    raw.add("free", free);
-    mem.add("free", humanReadableUnits(free, df));
-    raw.add("total", total);
-    mem.add("total", humanReadableUnits(total, df));
-    raw.add("max", max);
-    mem.add("max", humanReadableUnits(max, df));
-    raw.add("used", used);
-    mem.add("used", humanReadableUnits(used, df) + " (%" + 
df.format(percentUsed) + ")");
-    raw.add("used%", percentUsed);
-
-    mem.add("raw", raw);
-    jvm.add("memory", mem);
-
-    // JMX properties -- probably should be moved to a different handler
-    SimpleOrderedMap<Object> jmx = new SimpleOrderedMap<>();
+    raw.free = free;
+    mem.free = humanReadableUnits(free, df);
+    raw.total = total;
+    mem.total = humanReadableUnits(total, df);
+    raw.max = max;
+    mem.max = humanReadableUnits(max, df);
+    raw.used = used;
+    mem.used = humanReadableUnits(used, df) + " (%" + df.format(percentUsed) + 
")";
+    raw.usedPercent = percentUsed;
+
+    mem.raw = raw;
+    jvm.memory = mem;
+
+    // JMX properties
+    NodeSystemResponse.JvmJmx jmx = new NodeSystemResponse.JvmJmx();
     try {
       RuntimeMXBean mx = ManagementFactory.getRuntimeMXBean();
       if (mx.isBootClassPathSupported()) {
-        jmx.add("bootclasspath", mx.getBootClassPath());
+        jmx.classpath = mx.getBootClassPath();
       }
-      jmx.add("classpath", mx.getClassPath());
+      jmx.classpath = mx.getClassPath();
 
       // the input arguments passed to the Java virtual machine
       // which does not include the arguments to the main method.
-      jmx.add("commandLineArgs", getInputArgumentsRedacted(nodeConfig, mx));
+      NodeConfig nodeConfig = cc != null ? cc.getNodeConfig() : null;
+      jmx.commandLineArgs = getInputArgumentsRedacted(nodeConfig, mx);
 
-      jmx.add("startTime", new Date(mx.getStartTime()));
-      jmx.add("upTimeMS", mx.getUptime());
+      jmx.startTime = new Date(mx.getStartTime());
+      jmx.upTimeMS = mx.getUptime();
 
     } catch (Exception e) {
       log.warn("Error getting JMX properties", e);
     }
-    jvm.add("jmx", jmx);
+    jvm.jmx = jmx;
     return jvm;
   }
 
   /** Get Security Info */
-  public SimpleOrderedMap<Object> getSecurityInfo(SolrQueryRequest req) {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
+  public NodeSystemResponse.Security getSecurityInfo() {
+    NodeSystemResponse.Security info = new NodeSystemResponse.Security();
 
     if (cc != null) {
       if (cc.getAuthenticationPlugin() != null) {
-        info.add("authenticationPlugin", 
cc.getAuthenticationPlugin().getName());
+        info.authenticationPlugin = cc.getAuthenticationPlugin().getName();
       }
       if (cc.getAuthorizationPlugin() != null) {
-        info.add("authorizationPlugin", 
cc.getAuthorizationPlugin().getClass().getName());
+        info.authorizationPlugin = 
cc.getAuthorizationPlugin().getClass().getName();
       }
     }
 
     if (req.getUserPrincipal() != null
         && req.getUserPrincipal() != 
PKIAuthenticationPlugin.CLUSTER_MEMBER_NODE) {
       // User principal
-      info.add("username", req.getUserPrincipal().getName());
+      info.username = req.getUserPrincipal().getName();
 
       // Mapped roles for this principal
-      @SuppressWarnings("resource")
+      // @SuppressWarnings("resource")
       AuthorizationPlugin auth = cc == null ? null : 
cc.getAuthorizationPlugin();
       if (auth instanceof RuleBasedAuthorizationPluginBase rbap) {
         Set<String> roles = rbap.getUserRoles(req.getUserPrincipal());
-        info.add("roles", roles);
+        info.roles = roles;
         if (roles == null) {
-          info.add("permissions", Set.of());
+          info.permissions = Set.of();
         } else {
-          info.add(
-              "permissions",
+          info.permissions =
               rbap.getPermissionNamesForRoles(
-                  Stream.concat(roles.stream(), Stream.of("*", 
null)).collect(Collectors.toSet())));
+                  Stream.concat(roles.stream(), Stream.of("*", 
null)).collect(Collectors.toSet()));
         }
       }
     }
@@ -396,65 +322,96 @@ public class SystemInfoHandler extends RequestHandlerBase 
{
     if (cc != null && cc.getZkController() != null) {
       String urlScheme =
           
cc.getZkController().zkStateReader.getClusterProperty(ZkStateReader.URL_SCHEME, 
"http");
-      info.add("tls", ZkStateReader.HTTPS.equals(urlScheme));
+      info.tls = ZkStateReader.HTTPS.equals(urlScheme);
     }
 
     return info;
   }
 
-  private static SimpleOrderedMap<Object> getLuceneInfo() {
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
+  /** Get Lucene and Solr versions */
+  public NodeSystemResponse.Lucene getLuceneInfo() {
+    NodeSystemResponse.Lucene info = new NodeSystemResponse.Lucene();
 
     Package p = SolrCore.class.getPackage();
+    String specVersion = p.getSpecificationVersion();
+    String implVersion = p.getImplementationVersion();
+    // non-null mostly for testing
+    info.solrSpecVersion = specVersion == null ? SolrVersion.LATEST_STRING : 
specVersion;
+    info.solrImplVersion =
+        implVersion == null ? SolrVersion.LATEST.getPrereleaseVersion() : 
implVersion;
 
-    info.add("solr-spec-version", p.getSpecificationVersion());
-    info.add("solr-impl-version", p.getImplementationVersion());
-
-    info.add("lucene-spec-version", Version.LATEST.toString());
-    info.add("lucene-impl-version", Version.getPackageImplementationVersion());
+    info.luceneSpecVersion = Version.LATEST.toString();
+    info.luceneImplVersion = Version.getPackageImplementationVersion();
 
     return info;
   }
 
-  //////////////////////// SolrInfoMBeans methods //////////////////////
+  /** Get GPU info */
+  public NodeSystemResponse.GPU getGpuInfo() {
+    NodeSystemResponse.GPU gpuInfo = new NodeSystemResponse.GPU();
+    gpuInfo.available = false; // set below if available
 
-  @Override
-  public String getDescription() {
-    return "Get System Info";
-  }
+    try {
+      GpuMetricsProvider provider = cc.getGpuMetricsProvider();
 
-  @Override
-  public Category getCategory() {
-    return Category.ADMIN;
-  }
+      if (provider == null) {
+        return gpuInfo;
+      }
 
-  private static final long ONE_KB = 1024;
-  private static final long ONE_MB = ONE_KB * ONE_KB;
-  private static final long ONE_GB = ONE_KB * ONE_MB;
+      long gpuCount = provider.getGpuCount();
+      if (gpuCount > 0) {
+        gpuInfo.available = true;
+        gpuInfo.count = gpuCount;
+
+        long gpuMemoryTotal = provider.getGpuMemoryTotal();
+        long gpuMemoryUsed = provider.getGpuMemoryUsed();
+        long gpuMemoryFree = provider.getGpuMemoryFree();
+
+        if (gpuMemoryTotal > 0) {
+          NodeSystemResponse.MemoryRaw memory = new 
NodeSystemResponse.MemoryRaw();
+          memory.total = gpuMemoryTotal;
+          memory.used = gpuMemoryUsed;
+          memory.free = gpuMemoryFree;
+          gpuInfo.memory = memory;
+        }
+
+        var devices = provider.getGpuDevices();
+        if (devices != null && devices.size() > 0) {
+          gpuInfo.devices = devices;
+        }
+      }
+
+    } catch (Exception e) {
+      log.warn("Failed to get GPU information", e);
+    }
+
+    return gpuInfo;
+  }
 
   /** Return good default units based on byte size. */
-  private static String humanReadableUnits(long bytes, DecimalFormat df) {
+  private String humanReadableUnits(long bytes, DecimalFormat df) {
     String newSizeAndUnits;
 
     if (bytes / ONE_GB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_GB) + " GB";
+      newSizeAndUnits = String.valueOf(df.format((float) bytes / ONE_GB)) + " 
GB";
     } else if (bytes / ONE_MB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_MB) + " MB";
+      newSizeAndUnits = String.valueOf(df.format((float) bytes / ONE_MB)) + " 
MB";
     } else if (bytes / ONE_KB > 0) {
-      newSizeAndUnits = df.format((float) bytes / ONE_KB) + " KB";
+      newSizeAndUnits = String.valueOf(df.format((float) bytes / ONE_KB)) + " 
KB";
     } else {
-      newSizeAndUnits = bytes + " bytes";
+      newSizeAndUnits = String.valueOf(bytes) + " bytes";
     }
 
     return newSizeAndUnits;
   }
 
-  private static List<String> getInputArgumentsRedacted(NodeConfig nodeConfig, 
RuntimeMXBean mx) {
+  private List<String> getInputArgumentsRedacted(NodeConfig nodeConfig, 
RuntimeMXBean mx) {
     List<String> list = new ArrayList<>();
     for (String arg : mx.getInputArguments()) {
       if (arg.startsWith("-D")
           && arg.contains("=")
-          && nodeConfig.isSysPropHidden(arg.substring(2, arg.indexOf('=')))) {
+          && (nodeConfig != null
+              && nodeConfig.isSysPropHidden(arg.substring(2, 
arg.indexOf('='))))) {
         list.add(
             String.format(
                 Locale.ROOT,
@@ -468,62 +425,107 @@ public class SystemInfoHandler extends 
RequestHandlerBase {
     return list;
   }
 
-  @Override
-  public Collection<Api> getApis() {
-    return AnnotatedApi.getApis(new NodeSystemInfoAPI(this));
-  }
-
-  @Override
-  public Boolean registerV2() {
-    return Boolean.TRUE;
+  /**
+   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
+   * property name and its current value.
+   *
+   * @param obj an instance of MXBean
+   * @param interfaces interfaces that it may implement. Each interface will 
be tried in turn, and
+   *     only if it exists and if it contains unique properties then they will 
be added as metrics.
+   * @param consumer consumer for each property name and value
+   * @param <T> formal type
+   */
+  private <T extends PlatformManagedObject> void forEachGetterValue(
+      T obj, String[] interfaces, BiConsumer<String, Object> consumer) {
+    for (String clazz : interfaces) {
+      try {
+        final Class<? extends PlatformManagedObject> intf =
+            Class.forName(clazz).asSubclass(PlatformManagedObject.class);
+        forEachGetterValue(obj, intf, consumer);
+      } catch (ClassNotFoundException e) {
+        // ignore
+      }
+    }
   }
 
-  private SimpleOrderedMap<Object> getGpuInfo(SolrQueryRequest req) {
-    SimpleOrderedMap<Object> gpuInfo = new SimpleOrderedMap<>();
+  /**
+   * Iterates over properties of the given MXBean and invokes the provided 
consumer with each
+   * property name and its current value.
+   *
+   * @param obj an instance of MXBean
+   * @param intf MXBean interface, one of {@link PlatformManagedObject}-s
+   * @param consumer consumer for each property name and value
+   * @param <T> formal type
+   */
+  // protected & static : ref. test in NodeSystemInfoProviderTest
+  protected static <T extends PlatformManagedObject> void forEachGetterValue(
+      T obj, Class<? extends T> intf, BiConsumer<String, Object> consumer) {
+    if (intf.isInstance(obj)) {
+      BeanInfo beanInfo =
+          beanInfos.computeIfAbsent(
+              intf,
+              clazz -> {
+                try {
+                  return Introspector.getBeanInfo(
+                      clazz, clazz.getSuperclass(), 
Introspector.IGNORE_ALL_BEANINFO);
 
-    try {
-      GpuMetricsProvider provider = 
getCoreContainer(req).getGpuMetricsProvider();
+                } catch (IntrospectionException e) {
+                  log.warn("Unable to fetch properties of MXBean {}", 
obj.getClass().getName());
+                  return null;
+                }
+              });
 
-      if (provider == null) {
-        gpuInfo.add("available", false);
-        return gpuInfo;
+      // if BeanInfo retrieval failed, return early
+      if (beanInfo == null) {
+        return;
       }
+      for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
+        try {
+          Method readMethod = desc.getReadMethod();
+          if (readMethod == null) {
+            continue; // skip properties without a read method
+          }
 
-      long gpuCount = provider.getGpuCount();
-      if (gpuCount > 0) {
-        gpuInfo.add("available", true);
-        gpuInfo.add("count", gpuCount);
-
-        long gpuMemoryTotal = provider.getGpuMemoryTotal();
-        long gpuMemoryUsed = provider.getGpuMemoryUsed();
-        long gpuMemoryFree = provider.getGpuMemoryFree();
-
-        if (gpuMemoryTotal > 0) {
-          SimpleOrderedMap<Object> memory = new SimpleOrderedMap<>();
-          memory.add("total", gpuMemoryTotal);
-          memory.add("used", gpuMemoryUsed);
-          memory.add("free", gpuMemoryFree);
-          gpuInfo.add("memory", memory);
-        }
-
-        var devices = provider.getGpuDevices();
-        if (devices != null && devices.size() > 0) {
-          gpuInfo.add("devices", devices);
+          final String name = desc.getName();
+          Object value = readMethod.invoke(obj);
+          consumer.accept(name, value);
+        } catch (Exception e) {
+          // didn't work, skip it...
         }
-      } else {
-        gpuInfo.add("available", false);
       }
-
-    } catch (Exception e) {
-      log.warn("Failed to get GPU information", e);
-      gpuInfo.add("available", false);
     }
-
-    return gpuInfo;
   }
 
-  @Override
-  public Name getPermissionName(AuthorizationContext request) {
-    return Name.CONFIG_READ_PERM;
+  private void initHostname() {
+    if (!EnvUtils.getPropertyAsBool(REVERSE_DNS_OF_LOCALHOST_SYSPROP, true)) {
+      log.info(
+          "Resolving canonical hostname for local host prevented due to '{}' 
sysprop",
+          REVERSE_DNS_OF_LOCALHOST_SYSPROP);
+      hostname = null;
+      return;
+    }
+
+    RTimer timer = new RTimer();
+    try {
+      InetAddress addr = InetAddress.getLocalHost();
+      hostname = addr.getCanonicalHostName();
+    } catch (Exception e) {
+      log.warn(
+          "Unable to resolve canonical hostname for local host, possible DNS 
misconfiguration. Set the '{}' sysprop to false on startup to prevent future 
lookups if DNS can not be fixed.",
+          REVERSE_DNS_OF_LOCALHOST_SYSPROP,
+          e);
+      hostname = null;
+      return;
+    } finally {
+      timer.stop();
+
+      if (15000D < timer.getTime()) {
+        String readableTime = String.format(Locale.ROOT, "%.3f", 
(timer.getTime() / 1000));
+        log.warn(
+            "Resolving canonical hostname for local host took {} seconds, 
possible DNS misconfiguration. Set the '{}' sysprop to false on startup to 
prevent future lookups if DNS can not be fixed.",
+            readableTime,
+            REVERSE_DNS_OF_LOCALHOST_SYSPROP);
+      }
+    }
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeSystemInfo.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeSystemInfo.java
new file mode 100644
index 00000000000..5885489f78e
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/api/GetNodeSystemInfo.java
@@ -0,0 +1,81 @@
+/*
+ * 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.handler.admin.api;
+
+import jakarta.inject.Inject;
+import java.lang.invoke.MethodHandles;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.client.api.endpoint.NodeSystemInfoApi;
+import org.apache.solr.client.api.model.NodeSystemResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.admin.AdminHandlersProxy;
+import org.apache.solr.handler.admin.SystemInfoProvider;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.PermissionNameProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Implementation for {@link NodeSystemInfoApi} */
+public class GetNodeSystemInfo extends JerseyResource implements 
NodeSystemInfoApi {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final CoreContainer coreContainer;
+  private final SolrQueryRequest solrQueryRequest;
+  private final SolrQueryResponse solrQueryResponse;
+
+  @Inject
+  public GetNodeSystemInfo(SolrQueryRequest solrQueryRequest, 
SolrQueryResponse solrQueryResponse) {
+    this.coreContainer = solrQueryRequest.getCoreContainer();
+    this.solrQueryRequest = solrQueryRequest;
+    this.solrQueryResponse = solrQueryResponse;
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.CONFIG_READ_PERM)
+  public NodeSystemResponse getNodeSystemInfo(String nodes) {
+    solrQueryResponse.setHttpCaching(false);
+
+    if (proxyToNodes()) {
+      return null; // Request handled via proxying
+    }
+
+    SystemInfoProvider provider = new SystemInfoProvider(solrQueryRequest);
+    NodeSystemResponse response = 
instantiateJerseyResponse(NodeSystemResponse.class);
+    provider.getNodeSystemInfo(response);
+    if (log.isTraceEnabled()) {
+      log.trace("Node {}, core root: {}", response.node, response.coreRoot);
+    }
+    return response;
+  }
+
+  private boolean proxyToNodes() {
+    try {
+      if (coreContainer != null
+          && AdminHandlersProxy.maybeProxyToNodes(
+              "V2", solrQueryRequest, solrQueryResponse, coreContainer)) {
+        return true; // Request was proxied to other node
+      }
+    } catch (Exception e) {
+      throw new SolrException(
+          SolrException.ErrorCode.SERVER_ERROR, "Error occurred while proxying 
to other node", e);
+    }
+    return false;
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeSystemInfoAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/NodeSystemInfoAPI.java
deleted file mode 100644
index d7df2cfa0cd..00000000000
--- 
a/solr/core/src/java/org/apache/solr/handler/admin/api/NodeSystemInfoAPI.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.handler.admin.api;
-
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
-import static 
org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
-
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.handler.admin.SystemInfoHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
-
-/**
- * V2 API for getting "system" information from the receiving node.
- *
- * <p>This includes current resource utilization, information about the 
installation (location,
- * version, etc.), and JVM settings.
- *
- * <p>This API (GET /v2/node/system) is analogous to the v1 /admin/info/system.
- */
-public class NodeSystemInfoAPI {
-  private final SystemInfoHandler handler;
-
-  public NodeSystemInfoAPI(SystemInfoHandler handler) {
-    this.handler = handler;
-  }
-
-  @EndPoint(
-      path = {"/node/system"},
-      method = GET,
-      permission = CONFIG_READ_PERM)
-  public void getSystemInformation(SolrQueryRequest req, SolrQueryResponse 
rsp) throws Exception {
-    handler.handleRequestBody(req, rsp);
-  }
-}
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 013d3639e55..1076f7a3bd3 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/RepositoryManager.java
@@ -46,13 +46,11 @@ 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.SystemApi;
 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;
@@ -141,18 +139,11 @@ public class RepositoryManager {
   }
 
   public void addKey(byte[] key, String destinationKeyFilename) throws 
Exception {
-    // get solr_home directory from info servlet
-    // 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);
+    final var sysResponse = new 
SystemApi.GetNodeSystemInfo().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(sysResponse.getSolrHome()));
+    PackageUtils.uploadKey(key, path, Path.of(sysResponse.solrHome));
     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/handler/admin/NodeSystemInfoProviderTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/NodeSystemInfoProviderTest.java
new file mode 100644
index 00000000000..13b3ee4ce3e
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/NodeSystemInfoProviderTest.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.handler.admin;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.util.Arrays;
+import org.apache.lucene.util.Version;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.api.model.NodeSystemResponse;
+import org.apache.solr.client.api.util.SolrVersion;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequestBase;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+
+public class NodeSystemInfoProviderTest extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-minimal.xml", "schema.xml");
+  }
+
+  public void testMagickGetter() {
+    OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
+
+    // make one directly
+    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
+    info.add("name", os.getName());
+    info.add("version", os.getVersion());
+    info.add("arch", os.getArch());
+
+    // make another using MetricUtils.addMXBeanMetrics()
+    SimpleOrderedMap<Object> info2 = new SimpleOrderedMap<>();
+    SystemInfoProvider.forEachGetterValue(os, OperatingSystemMXBean.class, 
info2::add);
+
+    // make sure they got the same thing
+    for (String p : Arrays.asList("name", "version", "arch")) {
+      assertEquals(info.get(p), info2.get(p));
+    }
+  }
+
+  public void testGetNodeSystemInfo() {
+    SolrQueryRequest req = new SolrQueryRequestBase(h.getCore(), new 
ModifiableSolrParams()) {};
+    SystemInfoProvider provider = new SystemInfoProvider(req);
+    NodeSystemResponse info = provider.getNodeSystemInfo(new 
NodeSystemResponse());
+
+    Assert.assertNotNull(info);
+    // these can be validated
+    Assert.assertEquals(h.getCoreContainer().getSolrHome().toString(), 
info.solrHome);
+    
Assert.assertEquals(h.getCoreContainer().getCoreRootDirectory().toString(), 
info.coreRoot);
+    Assert.assertNotNull(info.lucene);
+    Assert.assertNotNull(info.lucene.solrImplVersion);
+    Assert.assertEquals(info.lucene.solrImplVersion, 
SolrVersion.LATEST.getPrereleaseVersion());
+    Assert.assertNotNull(info.lucene.solrSpecVersion);
+    Assert.assertEquals(info.lucene.solrSpecVersion, 
SolrVersion.LATEST_STRING);
+    Assert.assertNotNull(info.lucene.luceneImplVersion);
+    Assert.assertEquals(info.lucene.luceneImplVersion, 
Version.getPackageImplementationVersion());
+    Assert.assertNotNull(info.lucene.luceneSpecVersion);
+    Assert.assertEquals(info.lucene.luceneSpecVersion, 
Version.LATEST.toString());
+    // these should be set
+    Assert.assertNotNull(info.mode);
+    Assert.assertNotNull(info.jvm);
+    Assert.assertNotNull(info.security);
+    Assert.assertNotNull(info.system);
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/SystemInfoHandlerTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/SystemInfoHandlerTest.java
deleted file mode 100644
index 363de728439..00000000000
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/SystemInfoHandlerTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.handler.admin;
-
-import java.lang.management.ManagementFactory;
-import java.lang.management.OperatingSystemMXBean;
-import java.util.Arrays;
-import org.apache.solr.SolrTestCase;
-import org.apache.solr.common.util.SimpleOrderedMap;
-
-public class SystemInfoHandlerTest extends SolrTestCase {
-
-  public void testMagickGetter() {
-
-    OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
-
-    // make one directly
-    SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-    info.add("name", os.getName());
-    info.add("version", os.getVersion());
-    info.add("arch", os.getArch());
-
-    // make another using MetricUtils.addMXBeanMetrics()
-    SimpleOrderedMap<Object> info2 = new SimpleOrderedMap<>();
-    SystemInfoHandler.forEachGetterValue(os, OperatingSystemMXBean.class, 
info2::add);
-
-    // make sure they got the same thing
-    for (String p : Arrays.asList("name", "version", "arch")) {
-      assertEquals(info.get(p), info2.get(p));
-    }
-  }
-}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodeSystemInfoTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodeSystemInfoTest.java
new file mode 100644
index 00000000000..e6cf199b543
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/GetNodeSystemInfoTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.handler.admin.api;
+
+import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP;
+
+import org.apache.lucene.util.Version;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.request.SystemApi;
+import org.apache.solr.common.util.EnvUtils;
+import org.apache.solr.util.ExternalPaths;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** Test {@link GetNodeSystemInfo}. */
+public class GetNodeSystemInfoTest extends SolrTestCaseJ4 {
+
+  @ClassRule public static SolrJettyTestRule solrTestRule = new 
SolrJettyTestRule();
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    EnvUtils.setProperty(
+        ALLOW_PATHS_SYSPROP, 
ExternalPaths.SERVER_HOME.toAbsolutePath().toString());
+    solrTestRule.startSolr(createTempDir());
+    solrTestRule
+        .newCollection(DEFAULT_TEST_CORENAME)
+        .withConfigSet(ExternalPaths.DEFAULT_CONFIGSET)
+        .create();
+  }
+
+  @Test
+  public void testGetNodeInfoHttp() throws Exception {
+    final var req = new SystemApi.GetNodeSystemInfo();
+
+    final var infoRsp = req.process(solrTestRule.getSolrClient(null));
+
+    // Test a few values, majority of field-level validation occurs in 
NodeSystemInfoProviderTest
+    assertEquals(0, infoRsp.responseHeader.status);
+    assertEquals("std", infoRsp.mode);
+    assertEquals(Version.LATEST.toString(), infoRsp.lucene.luceneSpecVersion);
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
index c0ca1bff0c0..1e7dc2c43e7 100644
--- 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2NodeAPIMappingTest.java
@@ -39,7 +39,6 @@ import org.apache.solr.handler.admin.HealthCheckHandler;
 import org.apache.solr.handler.admin.InfoHandler;
 import org.apache.solr.handler.admin.LoggingHandler;
 import org.apache.solr.handler.admin.PropertiesRequestHandler;
-import org.apache.solr.handler.admin.SystemInfoHandler;
 import org.apache.solr.handler.admin.ThreadDumpHandler;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequestBase;
@@ -55,7 +54,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
   private ArgumentCaptor<SolrQueryRequest> queryRequestCaptor;
   private CoreAdminHandler mockCoresHandler;
   private InfoHandler infoHandler;
-  private SystemInfoHandler mockSystemInfoHandler;
   private LoggingHandler mockLoggingHandler;
   private PropertiesRequestHandler mockPropertiesHandler;
   private HealthCheckHandler mockHealthCheckHandler;
@@ -70,14 +68,12 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
   public void setupApiBag() {
     mockCoresHandler = mock(CoreAdminHandler.class);
     infoHandler = mock(InfoHandler.class);
-    mockSystemInfoHandler = mock(SystemInfoHandler.class);
     mockLoggingHandler = mock(LoggingHandler.class);
     mockPropertiesHandler = mock(PropertiesRequestHandler.class);
     mockHealthCheckHandler = mock(HealthCheckHandler.class);
     mockThreadDumpHandler = mock(ThreadDumpHandler.class);
     queryRequestCaptor = ArgumentCaptor.forClass(SolrQueryRequest.class);
 
-    when(infoHandler.getSystemInfoHandler()).thenReturn(mockSystemInfoHandler);
     when(infoHandler.getLoggingHandler()).thenReturn(mockLoggingHandler);
     when(infoHandler.getPropertiesHandler()).thenReturn(mockPropertiesHandler);
     
when(infoHandler.getHealthCheckHandler()).thenReturn(mockHealthCheckHandler);
@@ -146,16 +142,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
     assertEquals("anyParamValue", v1Params.get("anyParamName"));
   }
 
-  @Test
-  public void testSystemInfoApiAllProperties() throws Exception {
-    final ModifiableSolrParams solrParams = new ModifiableSolrParams();
-    solrParams.add("anyParamName", "anyParamValue");
-    final SolrParams v1Params = captureConvertedSystemV1Params("/node/system", 
"GET", solrParams);
-
-    // All parameters are passed through to v1 API as-is.
-    assertEquals("anyParamValue", v1Params.get("anyParamName"));
-  }
-
   @Test
   public void testHealthCheckApiAllProperties() throws Exception {
     final ModifiableSolrParams solrParams = new ModifiableSolrParams();
@@ -175,11 +161,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
         path, method, new ModifiableSolrParams(), v2RequestBody, 
mockCoresHandler);
   }
 
-  private SolrParams captureConvertedSystemV1Params(
-      String path, String method, SolrParams inputParams) throws Exception {
-    return doCaptureParams(path, method, inputParams, null, 
mockSystemInfoHandler);
-  }
-
   private SolrParams captureConvertedPropertiesV1Params(
       String path, String method, SolrParams inputParams) throws Exception {
     return doCaptureParams(path, method, inputParams, null, 
mockPropertiesHandler);
@@ -232,7 +213,6 @@ public class V2NodeAPIMappingTest extends SolrTestCaseJ4 {
     apiBag.registerObject(new RejoinLeaderElectionAPI(coreHandler));
     apiBag.registerObject(new 
NodePropertiesAPI(infoHandler.getPropertiesHandler()));
     apiBag.registerObject(new 
NodeThreadsAPI(infoHandler.getThreadDumpHandler()));
-    apiBag.registerObject(new 
NodeSystemInfoAPI(infoHandler.getSystemInfoHandler()));
     apiBag.registerObject(new 
NodeHealthAPI(infoHandler.getHealthCheckHandler()));
   }
 }
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/system-info-handler.adoc
 
b/solr/solr-ref-guide/modules/configuration-guide/pages/system-info-handler.adoc
index 6d55650a948..5fe1359e2e7 100644
--- 
a/solr/solr-ref-guide/modules/configuration-guide/pages/system-info-handler.adoc
+++ 
b/solr/solr-ref-guide/modules/configuration-guide/pages/system-info-handler.adoc
@@ -18,17 +18,50 @@
 
 This API provides the same information displayed on the Dashboard.
 
-System information is available via:
+== Request
 
+"System"-level information for individual nodes can be fetched with the API 
below:
+
+[tabs#systeminformation]
+======
+V1 API::
++
+====
 [source,bash]
 ----
 http://localhost:8983/solr/admin/info/system
 ----
+====
+
+V2 API::
++
+====
+[source,bash]
+----
+http://localhost:8983/api/node/system
+----
+====
+======
+
+
+== Response
 
 The keys in the result are:
 
 `mode`::
-Either `solrcloud` or `std`, indicating the mode Solr is running in
+Either `solrcloud` or `std`, indicating the mode Solr is running in standalone 
mode.
+
+`solr_home`::
+Solr base directory.
+
+`core_root`::
+Solr core base directory.
+
+`zkHost`::
+Zookeeper host, when running in cloud mode.
+
+`node`::
+Node name, when running cloud mode.
 
 `lucene`::
 An object containing Solr and Lucene version information
@@ -39,18 +72,15 @@ An object containing information on the JVM
 `system`::
 Information on the system.
 
-`solr_home`::
-Solr base directory.  Only available under `/solr/admin/info/system`.
+`security`::
+Information on security.
 
-`zkHost`::
-Zookeeper host, when running in cloud mode.
-
-`node`::
-Node name, when running cloud mode.
+`gpu`::
+Information on GPUs.
 
-== Lucene and Solr Information Object
+=== Lucene and Solr Versions Information Object
 
-The `solr` key in the response is an object with these keys:
+The `lucene` key in the response is an object with these keys:
 
 `lucene-spec-version`::
 The specification version of the Lucene package.
@@ -64,7 +94,7 @@ The specification version of the Solr package.
 `solr-impl-version`::
 The implementation version of the Solr package.
 
-== JVM Information Object
+=== JVM Information Object
 
 The `jvm` key in the response is an object with these keys and more:
 
@@ -77,9 +107,9 @@ Name of the JVM.
 `memory`::
 An object containing memory usage information.
 
-== System Information Object
+=== System Information Object
 
-The `system` key in the response is an object with these keys and more:
+The `system` key in the response is a map of the underlying system 
information, containing these keys, and more:
 
 `name`::
 Operating system name.
@@ -90,170 +120,98 @@ The result of running `uname -a`.  Not available on 
Windows.
 `uptime`::
 The result of running `uptime`.  Not available on Windows.
 
-== System Information API Examples
+=== Security Information Object
 
-Retrieve system information from a node in cloud mode.
+The `security` key in the response is an object with these keys:
 
-[source.bash]
-----
-curl http://localhost:8983/solr/admin/info/system
-----
+`tls`::
+Flag to indicate if TLS is enabled or not. If disabled, this is the only key 
in the response.
 
-[source.json]
-----
-{
-  "responseHeader":{
-    "status":0,
-    "QTime":13},
-  "mode":"solrcloud",
-  "host":"localhost",
-  "zkHost":"192.168.32.3:2181",
-  "solr_home":"/var/solr/data",
-  "lucene":{
-    "solr-spec-version":"8.1.1",
-    "solr-impl-version":"8.1.1 fcbe46c28cef11bc058779afba09521de1b19bef - ab - 
2019-05-22 15:20:01",
-    "lucene-spec-version":"8.1.1",
-    "lucene-impl-version":"8.1.1 fcbe46c28cef11bc058779afba09521de1b19bef - ab 
- 2019-05-22 15:15:24"},
-  "jvm":{
-    "version":"11.0.3 11.0.3+7",
-    "name":"Oracle Corporation OpenJDK 64-Bit Server VM",
-    "spec":{
-      "vendor":"Oracle Corporation",
-      "name":"Java Platform API Specification",
-      "version":"11"},
-    "jre":{
-      "vendor":"Oracle Corporation",
-      "version":"11.0.3"},
-    "vm":{
-      "vendor":"Oracle Corporation",
-      "name":"OpenJDK 64-Bit Server VM",
-      "version":"11.0.3+7"},
-    "processors":4,
-    "memory":{
-      "free":"396 MB",
-      "total":"512 MB",
-      "max":"512 MB",
-      "used":"116 MB (%22.7)",
-      "raw":{
-        "free":415213560,
-        "total":536870912,
-        "max":536870912,
-        "used":121657352,
-        "used%":22.660447657108307}},
-    "jmx":{
-      "classpath":"start.jar",
-      "commandLineArgs":["-Xms512m",
-        "-Xmx512m",
-        "-XX:+UseG1GC",
-        "-XX:+PerfDisableSharedMem",
-        "-XX:+ParallelRefProcEnabled",
-        "-XX:MaxGCPauseMillis=250",
-        "-XX:+UseLargePages",
-        "-XX:+AlwaysPreTouch",
-        
"-Xlog:gc*:file=/var/solr/logs/solr_gc.log:time,uptime:filecount=9,filesize=20M",
-        "-Dcom.sun.management.jmxremote",
-        "-Dcom.sun.management.jmxremote.local.only=false",
-        "-Dcom.sun.management.jmxremote.ssl=false",
-        "-Dcom.sun.management.jmxremote.authenticate=false",
-        "-Dcom.sun.management.jmxremote.port=18983",
-        "-Dcom.sun.management.jmxremote.rmi.port=18983",
-        "-DzkClientTimeout=15000",
-        "-DzkHost=192.168.32.3:2181",
-        "-Dsolr.logs.dir=/var/solr/logs",
-        "-Dsolr.port.listen=8983",
-        "-DSTOP.PORT=7983",
-        "-DSTOP.KEY=solrrocks",
-        "-Duser.timezone=UTC",
-        "-Djetty.home=/opt/solr/server",
-        "-Dsolr.solr.home=/var/solr/data",
-        "-Dsolr.data.home=",
-        "-Dsolr.install.dir=/opt/solr",
-        
"-Dsolr.configset.default.confdir=/opt/solr/server/solr/configsets/_default/conf",
-        "-Dlog4j.configurationFile=file:/var/solr/log4j2.xml",
-        "-Xss256k",
-        "-Dsolr.jetty.https.port=8983"],
-      "startTime":"2019-07-18T11:16:00.769Z",
-      "upTimeMS":1339007}},
-  "system":{
-    "name":"Linux",
-    "arch":"amd64",
-    "availableProcessors":4,
-    "systemLoadAverage":0.92,
-    "version":"4.9.0-9-amd64",
-    "committedVirtualMemorySize":4317540352,
-    "freePhysicalMemorySize":117563392,
-    "freeSwapSpaceSize":11583721472,
-    "processCpuLoad":0.0,
-    "processCpuTime":42690000000,
-    "systemCpuLoad":0.0,
-    "totalPhysicalMemorySize":4005376000,
-    "totalSwapSpaceSize":12884897792,
-    "maxFileDescriptorCount":1048576,
-    "openFileDescriptorCount":225,
-    "uname":"Linux f0281c6ee880 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 
(2019-06-16) x86_64 GNU/Linux\n",
-    "uptime":" 11:38:19 up 28 days, 22:41,  0 users,  load average: 0.92, 
0.57, 0.51\n"},
-  "node":"172.17.0.4:8983_solr"}
-----
+`authenticationPlugin`::
+The name of the configured authentication plugin.
+
+`authorizationPlugin`::
+The name of the configured authorization plugin.
+
+`username`::
+The name of the current user.
+
+`roles`::
+The roles of the current user.
+
+`permissions`::
+The permissions of the current user.
+
+=== GPU Information Object
+
+The `gpu` key in the response is an object with these keys:
+
+`available`::
+Flag to indicate if GPU is available or not. If unavailable, this is the only 
key in the response.
+
+`count`::
+Count of available GPUs.
+
+`memory`::
+An object listing the 'free', 'total', and 'used' memory.
+
+`devices`::
+A listing of the GPU devices.
+
+== Examples
+
+Examples on this page refer to the V2 API.
+
+=== Retrieve system information from a node in cloud mode.
 
-Retrieve system information from a core, without cloud mode.
 
 [source.bash]
 ----
-curl http://localhost:8983/solr/gettingstarted/admin/system
+curl http://localhost:8983/api/node/system
 ----
 
 [source.json]
 ----
 {
-  "responseHeader":{
-    "status":0,
-    "QTime":23},
-  "core":{
-    "schema":"default-config",
-    "host":"fd7fbdff8b3e",
-    "now":"2019-07-18T11:56:51.472Z",
-    "start":"2019-07-18T11:54:52.509Z",
-    "directory":{
-      "cwd":"/opt/solr-8.1.1/server",
-      "instance":"/var/solr/data/gettingstarted",
-      "data":"/var/solr/data/gettingstarted/data",
-      "dirimpl":"org.apache.solr.core.NRTCachingDirectoryFactory",
-      "index":"/var/solr/data/gettingstarted/data/index"}},
-  "mode":"std",
-  "lucene":{
-    "solr-spec-version":"8.1.1",
-    "solr-impl-version":"8.1.1 fcbe46c28cef11bc058779afba09521de1b19bef - ab - 
2019-05-22 15:20:01",
-    "lucene-spec-version":"8.1.1",
-    "lucene-impl-version":"8.1.1 fcbe46c28cef11bc058779afba09521de1b19bef - ab 
- 2019-05-22 15:15:24"},
-  "jvm":{
-    "version":"11.0.3 11.0.3+7",
-    "name":"Oracle Corporation OpenJDK 64-Bit Server VM",
-    "spec":{
-      "vendor":"Oracle Corporation",
-      "name":"Java Platform API Specification",
-      "version":"11"},
-    "jre":{
-      "vendor":"Oracle Corporation",
-      "version":"11.0.3"},
-    "vm":{
-      "vendor":"Oracle Corporation",
-      "name":"OpenJDK 64-Bit Server VM",
-      "version":"11.0.3+7"},
-    "processors":4,
-    "memory":{
-      "free":"394.9 MB",
-      "total":"512 MB",
-      "max":"512 MB",
-      "used":"117.1 MB (%22.9)",
-      "raw":{
-        "free":414074904,
-        "total":536870912,
-        "max":536870912,
-        "used":122796008,
-        "used%":22.87253886461258}},
-    "jmx":{
-      "classpath":"start.jar",
-      "commandLineArgs":["-Xms512m",
+  "responseHeader": {
+    "status": 0,
+    "QTime": 4
+  },
+  "node": "localhost:8983_solr",
+  "mode": "solrcloud",
+  "zkHost": "127.0.0.1:9983",
+  "solr_home": "/opt/solr/example/cloud/node1/solr",
+  "core_root": "/opt/solr/example/cloud/node1/solr",
+  "lucene": {
+    "solr-spec-version": "11.0.0",
+    "solr-impl-version": "11.0.0-SNAPSHOT 
f096b9a8aa2e10b8d2e87d7b8699953544d0f4f6 [snapshot build, details omitted]",
+    "lucene-spec-version": "10.3.2",
+    "lucene-impl-version": "10.3.2 dadfd90b4401947f4d0387669dc94999fbb2c830 - 
2025-11-13 10:41:29"
+  },
+  "jvm": {
+    "name": "Ubuntu OpenJDK 64-Bit Server VM",
+    "version": "21.0.9 21.0.9+10-Ubuntu-124.04",
+    "processors": 4,
+    "jre": {
+      "vendor": "Ubuntu",
+      "version": "21.0.9"
+    },
+    "spec": {
+      "name": "Java Platform API Specification",
+      "vendor": "Oracle Corporation",
+      "version": "21"
+    },
+    "vm": {
+      "name": "OpenJDK 64-Bit Server VM",
+      "vendor": "Ubuntu",
+      "version": "21.0.9+10-Ubuntu-124.04"
+    },
+    "jmx": {
+      "classpath": "start.jar",
+      "startTime": 1771355675708,
+      "upTimeMS": 1695023,
+      "commandLineArgs": [
+        "-Xms512m",
         "-Xmx512m",
         "-XX:+UseG1GC",
         "-XX:+PerfDisableSharedMem",
@@ -261,44 +219,77 @@ curl 
http://localhost:8983/solr/gettingstarted/admin/system
         "-XX:MaxGCPauseMillis=250",
         "-XX:+UseLargePages",
         "-XX:+AlwaysPreTouch",
-        
"-Xlog:gc*:file=/var/solr/logs/solr_gc.log:time,uptime:filecount=9,filesize=20M",
-        "-Dcom.sun.management.jmxremote",
-        "-Dcom.sun.management.jmxremote.local.only=false",
-        "-Dcom.sun.management.jmxremote.ssl=false",
-        "-Dcom.sun.management.jmxremote.authenticate=false",
-        "-Dcom.sun.management.jmxremote.port=18983",
-        "-Dcom.sun.management.jmxremote.rmi.port=18983",
-        "-Dsolr.logs.dir=/var/solr/logs",
+        "-XX:+ExplicitGCInvokesConcurrent",
+        
"-Xlog:gc*:file=/opt/solr/../logs/solr_gc.log:time,uptime:filecount=9,filesize=20M",
+        "-Dsolr.jetty.inetaccess.includes=",
+        "-Dsolr.jetty.inetaccess.excludes=",
+        "-Dsolr.zookeeper.client.timeout=30000",
+        "-Dsolr.zookeeper.server.enabled=true",
+        "-Dsolr.logs.dir=/opt/solr/../logs",
         "-Dsolr.port.listen=8983",
         "-DSTOP.PORT=7983",
         "-DSTOP.KEY=solrrocks",
+        "-Djava.io.tmpdir=/tmp",
+        "-Dsolr.host.advertise=localhost",
         "-Duser.timezone=UTC",
+        "-XX:-OmitStackTraceInFastThrow",
+        "-XX:+CrashOnOutOfMemoryError",
+        "-XX:ErrorFile=/opt/solr/../logs/jvm_crash_%p.log",
         "-Djetty.home=/opt/solr/server",
-        "-Dsolr.solr.home=/var/solr/data",
-        "-Dsolr.data.home=",
-        "-Dsolr.install.dir=/opt/solr",
-        
"-Dsolr.configset.default.confdir=/opt/solr/server/solr/configsets/_default/conf",
-        "-Dlog4j.configurationFile=file:/var/solr/log4j2.xml",
+        "-Dsolr.solr.home=/opt/solr/example/cloud/node1/solr",
+        "-Dsolr.install.dir=/opt/solr/",
+        "-Dsolr.install.symDir=/opt/solr/",
+        "-Dlog4j.configurationFile=/opt/solr/server/resources/log4j2.xml",
         "-Xss256k",
-        "-Dsolr.jetty.https.port=8983"],
-      "startTime":"2019-07-16T05:52:16.213Z",
-      "upTimeMS":194675370}},
-  "system":{
-    "name":"Linux",
-    "arch":"amd64",
-    "availableProcessors":4,
-    "systemLoadAverage":0.88,
-    "version":"4.9.0-9-amd64",
-    "committedVirtualMemorySize":4306059264,
-    "freePhysicalMemorySize":144179200,
-    "freeSwapSpaceSize":11626409984,
-    "processCpuLoad":0.0,
-    "processCpuTime":557920000000,
-    "systemCpuLoad":0.0,
-    "totalPhysicalMemorySize":4005376000,
-    "totalSwapSpaceSize":12884897792,
-    "maxFileDescriptorCount":1048576,
-    "openFileDescriptorCount":223,
-    "uname":"Linux fd7fbdff8b3e 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u3 
(2019-06-16) x86_64 GNU/Linux\n",
-    "uptime":" 11:56:51 up 28 days, 23:00,  0 users,  load average: 0.88, 
0.65, 0.62\n"}}
+        "--add-modules=jdk.incubator.vector",
+        "--enable-native-access=ALL-UNNAMED",
+        "-Djava.security.manager",
+        "-Djava.security.policy=/opt/solr/server/etc/security.policy",
+        "-Djava.security.properties=/opt/solr/server/etc/security.properties",
+        "-Dsolr.internal.network.permission=*",
+        "-Dsolr.log.muteconsole"
+      ]
+      "memory": {
+        "free": "229.5 MB",
+        "total": "512 MB",
+        "max": "512 MB",
+        "used": "282.5 MB (%55.2)",
+        "raw": {
+          "free": 240673856,
+          "total": 536870912,
+          "used": 296197056,
+          "max": 536870912,
+          "used%": 55.171000957489014
+        }
+      }
+    },
+    "security": {
+      "tls": false
+    },
+    "gpu": {
+      "available": false,
+      "count": 0
+    },
+    "system": {
+      "freePhysicalMemorySize": "2985586688",
+      "totalMemorySize": "8307912704",
+      "availableProcessors": "4",
+      "freeMemorySize": "2985586688",
+      "openFileDescriptorCount": "196",
+      "freeSwapSpaceSize": "2660519936",
+      "maxFileDescriptorCount": "65536",
+      "version": "6.14.0-33-generic",
+      "totalSwapSpaceSize": "4294963200",
+      "committedVirtualMemorySize": "4357632000",
+      "cpuLoad": "0.06624527740181138",
+      "systemLoadAverage": "0.3193359375",
+      "processCpuLoad": "0.004622084725943523",
+      "systemCpuLoad": "0.0",
+      "processCpuTime": "43450000000",
+      "name": "Linux",
+      "totalPhysicalMemorySize": "8307912704",
+      "arch": "amd64"
+    }
+  }
+}
 ----
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
index 345c0650256..08a9dd0c107 100644
--- 
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
@@ -32,13 +32,12 @@ public class SystemInfoRequest extends 
SolrRequest<SystemInfoResponse> {
 
   /** 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"
+   * @param path the HTTP path to use for this request. Defaults to the v1 
path,
+   *     "/admin/info/system"
    */
   public SystemInfoRequest(String path) {
     this(path, new ModifiableSolrParams());
@@ -52,8 +51,8 @@ public class SystemInfoRequest extends 
SolrRequest<SystemInfoResponse> {
   }
 
   /**
-   * @param path the HTTP path to use for this request. Supports V1 
"/admin/info/system" (default)
-   *     or V2 "/node/system"
+   * @param path the HTTP path to use for this request. Defaults to the v1 
path,
+   *     "/admin/info/system"
    * @param params query parameter names and values for making this request.
    */
   public SystemInfoRequest(String path, SolrParams params) {
@@ -70,14 +69,4 @@ public class SystemInfoRequest extends 
SolrRequest<SystemInfoResponse> {
   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
index 71274bfbf90..18cd0a0a9d5 100644
--- 
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
@@ -30,7 +30,9 @@ public class SystemInfoResponse extends SolrResponseBase {
 
   private static final long serialVersionUID = 1L;
 
-  private final Map<String, NodeSystemResponse> nodesInfo = new HashMap<>();
+  // AdminHandlersProxy wraps nodes responses in a map.
+  // Mimic that here, even if the response might be just a single node.
+  protected final Map<String, NodeSystemResponse> nodesInfo = new HashMap<>();
 
   public SystemInfoResponse(NamedList<Object> namedList) {
     if (namedList == null) throw new IllegalArgumentException("Null NamedList 
is not allowed.");
@@ -41,19 +43,24 @@ public class SystemInfoResponse extends SolrResponseBase {
   public void setResponse(NamedList<Object> response) {
     if (getResponse() == null) {
       super.setResponse(response);
+      parseResponse(response);
     } else {
       assert response.equals(getResponse());
       return;
     }
+  }
 
-    if (getResponse().get("node") == null) {
-      // multi-nodes response, NamedList of 
"host:port_solr"->NodeSystemResponse
+  /** Parse the V1 response */
+  @SuppressWarnings("unchecked")
+  protected void parseResponse(NamedList<Object> response) {
+    if (response.get("node") == null) {
+      // multi-nodes response, NamedList of "host:port_solr"-> 
NodeSystemResponse
       for (Entry<String, Object> node : response) {
         if (node.getKey().endsWith("_solr")) {
           nodesInfo.put(
               node.getKey(),
               JacksonContentWriter.DEFAULT_MAPPER.convertValue(
-                  node.getValue(), NodeSystemResponse.class));
+                  removeHeader((NamedList<Object>) node.getValue()), 
NodeSystemResponse.class));
         }
       }
 
@@ -62,7 +69,8 @@ public class SystemInfoResponse extends SolrResponseBase {
       if (nodesInfo.isEmpty()) {
         nodesInfo.put(
             null,
-            JacksonContentWriter.DEFAULT_MAPPER.convertValue(response, 
NodeSystemResponse.class));
+            JacksonContentWriter.DEFAULT_MAPPER.convertValue(
+                removeHeader(response), NodeSystemResponse.class));
       }
 
     } else {
@@ -70,10 +78,15 @@ public class SystemInfoResponse extends SolrResponseBase {
       nodesInfo.put(
           response.get("node").toString(),
           JacksonContentWriter.DEFAULT_MAPPER.convertValue(
-              getResponse(), NodeSystemResponse.class));
+              removeHeader(response), NodeSystemResponse.class));
     }
   }
 
+  private NamedList<Object> removeHeader(NamedList<Object> value) {
+    value.remove("responseHeader");
+    return value;
+  }
+
   /** Get the mode from a single node system info */
   public String getMode() {
     if (nodesInfo.size() == 1) {
@@ -217,7 +230,7 @@ public class SystemInfoResponse extends SolrResponseBase {
         .get();
   }
 
-  /** Get the {@code NodeSystemResponse} for a single node */
+  /** Get the {@code NodeSystemResponse.NodeSystemInfo} for a single node */
   public NodeSystemResponse getNodeResponse() {
     if (nodesInfo.size() == 1) {
       return nodesInfo.values().stream().findFirst().get();
@@ -232,8 +245,12 @@ public class SystemInfoResponse extends SolrResponseBase {
     return nodesInfo;
   }
 
-  /** Get the {@code NodeSystemResponse} for the given node name */
+  /** Get the {@code NodeSystemResponse.NodeSystemInfo} for the given node 
name */
   public NodeSystemResponse getNodeResponseForNode(String node) {
+    // in standalone mode, the key in the map is null
+    if (node == null && nodesInfo.size() == 1) {
+      return nodesInfo.values().stream().findFirst().get();
+    }
     return nodesInfo.get(node);
   }
 
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/response/json/JacksonDataBindResponseParser.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/response/json/JacksonDataBindResponseParser.java
index 954db55f10c..e98eb104cae 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/response/json/JacksonDataBindResponseParser.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/response/json/JacksonDataBindResponseParser.java
@@ -55,6 +55,9 @@ public class JacksonDataBindResponseParser<T> extends 
ResponseParser {
   // TODO it'd be nice if the ResponseParser could receive the mime type so it 
can parse
   //  accordingly, maybe json, cbor, smile
 
+  /**
+   * Parse the Json {@code stream} to the expected Java type, then, converts 
to a {@link NamedList}.
+   */
   @Override
   public NamedList<Object> processResponse(InputStream stream, String 
encoding) throws IOException {
     // TODO generalize to CBOR, Smile, ...
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 b26360864c5..052d0104bff 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,7 +205,6 @@ 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/response/SystemInfoResponseTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/response/SystemInfoResponseTest.java
index f129d63f10e..05e8d319370 100644
--- 
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
@@ -30,6 +30,7 @@ import org.junit.Test;
 
 public class SystemInfoResponseTest extends SolrCloudTestCase {
 
+  // private static MiniSolrCloudCluster cluster;
   private CloudSolrClient solrClient;
 
   @BeforeClass
@@ -44,6 +45,16 @@ public class SystemInfoResponseTest extends 
SolrCloudTestCase {
     solrClient = cluster.getSolrClient();
   }
 
+  @Test
+  public void testDefaultResponse() throws SolrServerException, IOException {
+    SystemInfoRequest req = new SystemInfoRequest();
+    SystemInfoResponse rsp = req.process(solrClient);
+
+    Assert.assertEquals(1, rsp.getAllNodeResponses().size());
+    Assert.assertEquals(1, rsp.getAllCoreRoots().size());
+    Assert.assertEquals(1, rsp.getAllModes().size());
+  }
+
   @Test
   public void testAllNodesResponse() throws SolrServerException, IOException {
     MapSolrParams params = new MapSolrParams(Map.of("nodes", "all"));
@@ -61,20 +72,29 @@ public class SystemInfoResponseTest extends 
SolrCloudTestCase {
     Assert.assertEquals(2, rsp.getAllNodeResponses().size());
     Assert.assertEquals(2, rsp.getAllCoreRoots().size());
     Assert.assertEquals(2, rsp.getAllModes().size());
+
+    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));
+    }
   }
 
   @Test
   public void testResponseForGivenNode() throws SolrServerException, 
IOException {
-    MapSolrParams params = new MapSolrParams(Map.of("nodes", "all"));
+    String queryNode = cluster.getJettySolrRunner(0).getNodeName();
+    MapSolrParams params = new MapSolrParams(Map.of("nodes", queryNode));
 
     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));
-    }
+    Assert.assertEquals(1, rsp.getAllNodeResponses().size());
+    Assert.assertEquals(1, rsp.getAllCoreRoots().size());
+    Assert.assertEquals(1, rsp.getAllModes().size());
+    String coreRoot = rsp.getCoreRootForNode(queryNode);
+    Assert.assertEquals(queryNode, rsp.getNodeForCoreRoot(coreRoot));
+    String solrHome = rsp.getCoreRootForNode(queryNode);
+    Assert.assertEquals(queryNode, rsp.getNodeForSolrHome(solrHome));
   }
 }

Reply via email to