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

malliaridis 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 e1f56b30c33 SOLR-17501: Move out CLI utils from SolrCLI (#2744)
e1f56b30c33 is described below

commit e1f56b30c33d9adfcb2c7aac1c01ef5393400c1e
Author: Christos Malliaridis <[email protected]>
AuthorDate: Tue Nov 19 22:06:44 2024 +0200

    SOLR-17501: Move out CLI utils from SolrCLI (#2744)
---
 .../core/src/java/org/apache/solr/cli/ApiTool.java |   2 +-
 .../src/java/org/apache/solr/cli/AssertTool.java   |  18 +-
 .../src/java/org/apache/solr/cli/AuthTool.java     |  16 +-
 .../src/java/org/apache/solr/cli/CLIUtils.java     | 351 +++++++++++++++++++++
 .../src/java/org/apache/solr/cli/ClusterTool.java  |   2 +-
 .../java/org/apache/solr/cli/CommonCLIOptions.java |   2 +-
 .../org/apache/solr/cli/ConfigSetDownloadTool.java |   4 +-
 .../org/apache/solr/cli/ConfigSetUploadTool.java   |   6 +-
 .../src/java/org/apache/solr/cli/ConfigTool.java   |   4 +-
 .../src/java/org/apache/solr/cli/CreateTool.java   |  20 +-
 .../src/java/org/apache/solr/cli/DeleteTool.java   |   8 +-
 .../src/java/org/apache/solr/cli/ExportTool.java   |   4 +-
 .../java/org/apache/solr/cli/HealthcheckTool.java  |  12 +-
 .../java/org/apache/solr/cli/LinkConfigTool.java   |   2 +-
 .../src/java/org/apache/solr/cli/PackageTool.java  |   6 +-
 .../src/java/org/apache/solr/cli/PostLogsTool.java |   2 +-
 .../src/java/org/apache/solr/cli/PostTool.java     |   8 +-
 .../java/org/apache/solr/cli/RunExampleTool.java   |   8 +-
 .../org/apache/solr/cli/SnapshotCreateTool.java    |   2 +-
 .../org/apache/solr/cli/SnapshotDeleteTool.java    |   2 +-
 .../org/apache/solr/cli/SnapshotDescribeTool.java  |   2 +-
 .../org/apache/solr/cli/SnapshotExportTool.java    |   2 +-
 .../java/org/apache/solr/cli/SnapshotListTool.java |   2 +-
 .../core/src/java/org/apache/solr/cli/SolrCLI.java | 296 +----------------
 .../src/java/org/apache/solr/cli/StatusTool.java   |  32 +-
 .../src/java/org/apache/solr/cli/StreamTool.java   |   4 +-
 .../java/org/apache/solr/cli/UpdateACLTool.java    |   2 +-
 .../src/java/org/apache/solr/cli/ZkCpTool.java     |   2 +-
 .../src/java/org/apache/solr/cli/ZkLsTool.java     |   4 +-
 .../src/java/org/apache/solr/cli/ZkMkrootTool.java |   4 +-
 .../src/java/org/apache/solr/cli/ZkMvTool.java     |   4 +-
 .../src/java/org/apache/solr/cli/ZkRmTool.java     |   4 +-
 .../apache/solr/packagemanager/PackageManager.java |   5 +-
 .../apache/solr/packagemanager/PackageUtils.java   |   4 +-
 .../src/test/org/apache/solr/cli/CLIUtilsTest.java | 144 +++++++++
 .../src/test/org/apache/solr/cli/SolrCLITest.java  |   9 -
 .../org/apache/solr/cli/TestSolrCLIRunExample.java |   2 +-
 .../apache/solr/cloud/SolrCloudExampleTest.java    |   3 +-
 38 files changed, 594 insertions(+), 410 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/cli/ApiTool.java 
b/solr/core/src/java/org/apache/solr/cli/ApiTool.java
index 02f893b3753..ede4de68971 100644
--- a/solr/core/src/java/org/apache/solr/cli/ApiTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ApiTool.java
@@ -79,7 +79,7 @@ public class ApiTool extends ToolBase {
     URI uri = new URI(url.replace("+", "%20"));
     String solrUrl = getSolrUrlFromUri(uri);
     String path = uri.getPath();
-    try (var solrClient = SolrCLI.getSolrClient(solrUrl, credentials)) {
+    try (var solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) {
       // For path parameter we need the path without the root so from the 
second / char
       // (because root can be configured)
       // E.g URL is http://localhost:8983/solr/admin/info/system path is
diff --git a/solr/core/src/java/org/apache/solr/cli/AssertTool.java 
b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
index b710d0934b2..b111ef7b481 100644
--- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
@@ -250,13 +250,13 @@ public class AssertTool extends ToolBase {
     if (cli.hasOption(IS_CLOUD_OPTION)) {
       ret +=
           assertSolrRunningInCloudMode(
-              SolrCLI.normalizeSolrUrl(cli.getOptionValue(IS_CLOUD_OPTION)),
+              CLIUtils.normalizeSolrUrl(cli.getOptionValue(IS_CLOUD_OPTION)),
               cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
     }
     if (cli.hasOption(IS_NOT_CLOUD_OPTION)) {
       ret +=
           assertSolrNotRunningInCloudMode(
-              
SolrCLI.normalizeSolrUrl(cli.getOptionValue(IS_NOT_CLOUD_OPTION)),
+              
CLIUtils.normalizeSolrUrl(cli.getOptionValue(IS_NOT_CLOUD_OPTION)),
               cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
     }
     return ret;
@@ -267,7 +267,7 @@ public class AssertTool extends ToolBase {
     try {
       status.waitToSeeSolrUp(url, credentials, timeoutMs, 
TimeUnit.MILLISECONDS);
     } catch (Exception se) {
-      if (SolrCLI.exceptionIsAuthRelated(se)) {
+      if (CLIUtils.exceptionIsAuthRelated(se)) {
         throw se;
       }
       return exitOrException(
@@ -284,10 +284,10 @@ public class AssertTool extends ToolBase {
     StatusTool status = new StatusTool();
     long timeout =
         System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeoutMs, 
TimeUnit.MILLISECONDS);
-    try (SolrClient solrClient = SolrCLI.getSolrClient(url, credentials)) {
+    try (SolrClient solrClient = CLIUtils.getSolrClient(url, credentials)) {
       NamedList<Object> response = solrClient.request(new 
HealthCheckRequest());
       Integer statusCode = (Integer) response.findRecursive("responseHeader", 
"status");
-      SolrCLI.checkCodeForAuthError(statusCode);
+      CLIUtils.checkCodeForAuthError(statusCode);
     } catch (IOException | SolrServerException e) {
       log.debug("Opening connection to {} failed, Solr does not seem to be 
running", url, e);
       return 0;
@@ -302,7 +302,7 @@ public class AssertTool extends ToolBase {
           timeout = 0; // stop looping
         }
       } catch (Exception se) {
-        if (SolrCLI.exceptionIsAuthRelated(se)) {
+        if (CLIUtils.exceptionIsAuthRelated(se)) {
           throw se;
         }
         return exitOrException(se.getMessage());
@@ -417,7 +417,7 @@ public class AssertTool extends ToolBase {
       status.waitToSeeSolrUp(url, credentials, timeoutMs, 
TimeUnit.MILLISECONDS);
       return true;
     } catch (Exception se) {
-      if (SolrCLI.exceptionIsAuthRelated(se)) {
+      if (CLIUtils.exceptionIsAuthRelated(se)) {
         throw se;
       }
       return false;
@@ -425,8 +425,8 @@ public class AssertTool extends ToolBase {
   }
 
   private static boolean runningSolrIsCloud(String url, String credentials) 
throws Exception {
-    try (final SolrClient client = SolrCLI.getSolrClient(url, credentials)) {
-      return SolrCLI.isCloudMode(client);
+    try (final SolrClient client = CLIUtils.getSolrClient(url, credentials)) {
+      return CLIUtils.isCloudMode(client);
     }
   }
 
diff --git a/solr/core/src/java/org/apache/solr/cli/AuthTool.java 
b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
index 817b0093748..45b609a2943 100644
--- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
@@ -194,7 +194,7 @@ public class AuthTool extends ToolBase {
 
         if (!updateIncludeFileOnly) {
           try {
-            zkHost = SolrCLI.getZkHost(cli);
+            zkHost = CLIUtils.getZkHost(cli);
           } catch (Exception ex) {
             CLIO.out(
                 "Unable to access ZooKeeper. Please add the following 
security.json to ZooKeeper (in case of SolrCloud):\n"
@@ -214,7 +214,7 @@ public class AuthTool extends ToolBase {
 
           // check if security is already enabled or not
           if (!zkInaccessible) {
-            try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) 
{
+            try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, 
zkHost)) {
               checkSecurityJsonExists(zkClient);
             } catch (Exception ex) {
               CLIO.out(
@@ -229,7 +229,7 @@ public class AuthTool extends ToolBase {
         if (!updateIncludeFileOnly) {
           if (!zkInaccessible) {
             echoIfVerbose("Uploading following security.json: " + 
securityJson);
-            try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) 
{
+            try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, 
zkHost)) {
               zkClient.setData(
                   "/security.json", 
securityJson.getBytes(StandardCharsets.UTF_8), true);
             } catch (Exception ex) {
@@ -309,7 +309,7 @@ public class AuthTool extends ToolBase {
 
         if (!updateIncludeFileOnly) {
           try {
-            zkHost = SolrCLI.getZkHost(cli);
+            zkHost = CLIUtils.getZkHost(cli);
           } catch (Exception ex) {
             if (cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION)) {
               CLIO.out(
@@ -332,7 +332,7 @@ public class AuthTool extends ToolBase {
           }
 
           // check if security is already enabled or not
-          try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+          try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
             checkSecurityJsonExists(zkClient);
           }
         }
@@ -381,7 +381,7 @@ public class AuthTool extends ToolBase {
 
         if (!updateIncludeFileOnly) {
           echoIfVerbose("Uploading following security.json: " + securityJson);
-          try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+          try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
             zkClient.setData("/security.json", 
securityJson.getBytes(StandardCharsets.UTF_8), true);
           }
         }
@@ -460,7 +460,7 @@ public class AuthTool extends ToolBase {
   private void clearSecurityJson(CommandLine cli, boolean 
updateIncludeFileOnly) throws Exception {
     String zkHost;
     if (!updateIncludeFileOnly) {
-      zkHost = SolrCLI.getZkHost(cli);
+      zkHost = CLIUtils.getZkHost(cli);
       if (zkHost == null) {
         stdout.print("ZK Host not found. Solr should be running in cloud 
mode.");
         SolrCLI.exit(1);
@@ -468,7 +468,7 @@ public class AuthTool extends ToolBase {
 
       echoIfVerbose("Uploading following security.json: {}");
 
-      try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+      try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
         zkClient.setData("/security.json", 
"{}".getBytes(StandardCharsets.UTF_8), true);
       }
     }
diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java 
b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
new file mode 100644
index 00000000000..7f4c0dc648b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java
@@ -0,0 +1,351 @@
+/*
+ * 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.cli;
+
+import static org.apache.solr.common.SolrException.ErrorCode.FORBIDDEN;
+import static org.apache.solr.common.SolrException.ErrorCode.UNAUTHORIZED;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.common.params.CommonParams.SYSTEM_INFO_PATH;
+
+import java.io.IOException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.impl.SolrZkClientTimeout;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.util.EnvUtils;
+import org.apache.solr.common.util.NamedList;
+
+/** Utility class that holds various helper methods for the CLI. */
+public final class CLIUtils {
+
+  private CLIUtils() {}
+
+  public static String RED = "\u001B[31m";
+
+  public static String GREEN = "\u001B[32m";
+
+  public static String YELLOW = "\u001B[33m";
+
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  public static String getDefaultSolrUrl() {
+    // note that ENV_VAR syntax (and the env vars too) are mapped to env.var 
sys props
+    String scheme = EnvUtils.getProperty("solr.url.scheme", "http");
+    String host = EnvUtils.getProperty("solr.tool.host", "localhost");
+    String port = EnvUtils.getProperty("jetty.port", "8983"); // from 
SOLR_PORT env
+    return String.format(Locale.ROOT, "%s://%s:%s", 
scheme.toLowerCase(Locale.ROOT), host, port);
+  }
+
+  /**
+   * Determine if a request to Solr failed due to a communication error, which 
is generally
+   * retry-able.
+   */
+  public static boolean checkCommunicationError(Exception exc) {
+    Throwable rootCause = SolrException.getRootCause(exc);
+    return (rootCause instanceof SolrServerException || rootCause instanceof 
SocketException);
+  }
+
+  public static void checkCodeForAuthError(int code) {
+    if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
+      throw new SolrException(
+          SolrException.ErrorCode.getErrorCode(code),
+          "Solr requires authentication for request. Please supply valid 
credentials. HTTP code="
+              + code);
+    }
+  }
+
+  public static boolean exceptionIsAuthRelated(Exception exc) {
+    return (exc instanceof SolrException
+        && Arrays.asList(UNAUTHORIZED.code, 
FORBIDDEN.code).contains(((SolrException) exc).code()));
+  }
+
+  public static SolrClient getSolrClient(String solrUrl, String credentials, 
boolean barePath) {
+    // today we require all urls to end in /solr, however in the future we 
will need to support the
+    // /api url end point instead.   Eventually we want to have this method 
always
+    // return a bare url, and then individual calls decide if they are /solr 
or /api
+    // The /solr/ check is because sometimes a full url is passed in, like
+    // http://localhost:8983/solr/films_shard1_replica_n1/.
+    if (!barePath && !solrUrl.endsWith("/solr") && 
!solrUrl.contains("/solr/")) {
+      solrUrl = solrUrl + "/solr";
+    }
+    Http2SolrClient.Builder builder =
+        new Http2SolrClient.Builder(solrUrl)
+            .withMaxConnectionsPerHost(32)
+            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
+            .withOptionalBasicAuthCredentials(credentials);
+
+    return builder.build();
+  }
+
+  /**
+   * Helper method for all the places where we assume a /solr on the url.
+   *
+   * @param solrUrl The solr url that you want the client for
+   * @param credentials The username:password for basic auth.
+   * @return The SolrClient
+   */
+  public static SolrClient getSolrClient(String solrUrl, String credentials) {
+    return getSolrClient(solrUrl, credentials, false);
+  }
+
+  public static SolrClient getSolrClient(CommandLine cli, boolean barePath) 
throws Exception {
+    String solrUrl = normalizeSolrUrl(cli);
+    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
+    return getSolrClient(solrUrl, credentials, barePath);
+  }
+
+  public static SolrClient getSolrClient(CommandLine cli) throws Exception {
+    String solrUrl = normalizeSolrUrl(cli);
+    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
+    return getSolrClient(solrUrl, credentials, false);
+  }
+
+  /**
+   * Strips off the end of solrUrl any /solr when a legacy solrUrl like 
http://localhost:8983/solr
+   * is used, and warns those users. In the future we'll have urls ending with 
/api as well.
+   *
+   * @param solrUrl The user supplied url to Solr.
+   * @return the solrUrl in the format that Solr expects to see internally.
+   */
+  public static String normalizeSolrUrl(String solrUrl) {
+    return normalizeSolrUrl(solrUrl, true);
+  }
+
+  /**
+   * Strips off the end of solrUrl any /solr when a legacy solrUrl like 
http://localhost:8983/solr
+   * is used, and optionally logs a warning. In the future we'll have urls 
ending with /api as well.
+   *
+   * @param solrUrl The user supplied url to Solr.
+   * @param logUrlFormatWarning If a warning message should be logged about 
the url format
+   * @return the solrUrl in the format that Solr expects to see internally.
+   */
+  public static String normalizeSolrUrl(String solrUrl, boolean 
logUrlFormatWarning) {
+    if (solrUrl != null) {
+      URI uri = URI.create(solrUrl);
+      String urlPath = uri.getRawPath();
+      if (urlPath != null && urlPath.contains("/solr")) {
+        String newSolrUrl =
+            uri.resolve(urlPath.substring(0, urlPath.lastIndexOf("/solr") + 
1)).toString();
+        if (logUrlFormatWarning) {
+          CLIO.err(
+              "WARNING: URLs provided to this tool needn't include Solr's 
context-root (e.g. \"/solr\"). Such URLs are deprecated and support for them 
will be removed in a future release. Correcting from ["
+                  + solrUrl
+                  + "] to ["
+                  + newSolrUrl
+                  + "].");
+        }
+        solrUrl = newSolrUrl;
+      }
+      if (solrUrl.endsWith("/")) {
+        solrUrl = solrUrl.substring(0, solrUrl.length() - 1);
+      }
+    }
+    return solrUrl;
+  }
+
+  /**
+   * Get the base URL of a live Solr instance from either the --solr-url 
command-line option or from
+   * ZooKeeper.
+   */
+  public static String normalizeSolrUrl(CommandLine cli) throws Exception {
+    String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION);
+
+    if (solrUrl == null) {
+      String zkHost = cli.getOptionValue(CommonCLIOptions.ZK_HOST_OPTION);
+      if (zkHost == null) {
+        solrUrl = getDefaultSolrUrl();
+        CLIO.err(
+            "Neither --zk-host or --solr-url parameters provided so assuming 
solr url is "
+                + solrUrl
+                + ".");
+      } else {
+        try (CloudSolrClient cloudSolrClient = 
getCloudHttp2SolrClient(zkHost)) {
+          cloudSolrClient.connect();
+          Set<String> liveNodes = 
cloudSolrClient.getClusterState().getLiveNodes();
+          if (liveNodes.isEmpty())
+            throw new IllegalStateException(
+                "No live nodes found! Cannot determine 'solrUrl' from 
ZooKeeper: " + zkHost);
+
+          String firstLiveNode = liveNodes.iterator().next();
+          solrUrl = 
ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode);
+          solrUrl = normalizeSolrUrl(solrUrl, false);
+        }
+      }
+    }
+    solrUrl = normalizeSolrUrl(solrUrl);
+    return solrUrl;
+  }
+
+  /**
+   * Get the ZooKeeper connection string from either the zk-host command-line 
option or by looking
+   * it up from a running Solr instance based on the solr-url option.
+   */
+  public static String getZkHost(CommandLine cli) throws Exception {
+
+    String zkHost = cli.getOptionValue(CommonCLIOptions.ZK_HOST_OPTION);
+    if (zkHost != null && !zkHost.isBlank()) {
+      return zkHost;
+    }
+
+    try (SolrClient solrClient = getSolrClient(cli)) {
+      // hit Solr to get system info
+      NamedList<Object> systemInfo =
+          solrClient.request(
+              new GenericSolrRequest(SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
+
+      // convert raw JSON into user-friendly output
+      StatusTool statusTool = new StatusTool();
+      Map<String, Object> status = statusTool.reportStatus(systemInfo, 
solrClient);
+      @SuppressWarnings("unchecked")
+      Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
+      if (cloud != null) {
+        String zookeeper = (String) cloud.get("ZooKeeper");
+        if (zookeeper.endsWith("(embedded)")) {
+          zookeeper = zookeeper.substring(0, zookeeper.length() - 
"(embedded)".length());
+        }
+        zkHost = zookeeper;
+      }
+    }
+
+    return zkHost;
+  }
+
+  public static SolrZkClient getSolrZkClient(CommandLine cli, String zkHost) 
throws Exception {
+    if (zkHost == null) {
+      throw new IllegalStateException(
+          "Solr at "
+              + cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION)
+              + " is running in standalone server mode, this command can only 
be used when running in SolrCloud mode.\n");
+    }
+    return new SolrZkClient.Builder()
+        .withUrl(zkHost)
+        .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, 
TimeUnit.MILLISECONDS)
+        .build();
+  }
+
+  public static CloudHttp2SolrClient getCloudHttp2SolrClient(String zkHost) {
+    return getCloudHttp2SolrClient(zkHost, null);
+  }
+
+  public static CloudHttp2SolrClient getCloudHttp2SolrClient(
+      String zkHost, Http2SolrClient.Builder builder) {
+    return new CloudHttp2SolrClient.Builder(Collections.singletonList(zkHost), 
Optional.empty())
+        .withInternalClientBuilder(builder)
+        .build();
+  }
+
+  /**
+   * Extracts the port from the provided {@code solrUrl}. If a URL is provided 
with https scheme and
+   * not explicitly defines the port, the default port for HTTPS (443) is used.
+   *
+   * <p>If URL does not contain a port nor https as scheme, it falls back to 
port 80.
+   *
+   * @param solrUrl the URL to extract the port from
+   * @return The port that was found.
+   * @throws NullPointerException If solrUrl is null
+   * @throws URISyntaxException If the given string violates RFC 2396, as 
augmented by the above
+   *     deviations
+   */
+  public static int portFromUrl(String solrUrl) throws URISyntaxException {
+    URI uri = new URI(solrUrl);
+    int port = uri.getPort();
+    if (port == -1) {
+      return uri.getScheme().equals("https") ? 443 : 80;
+    } else {
+      return port;
+    }
+  }
+
+  public static boolean safeCheckCollectionExists(
+      String solrUrl, String collection, String credentials) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl, credentials)) {
+      NamedList<Object> existsCheckResult = solrClient.request(new 
CollectionAdminRequest.List());
+      @SuppressWarnings("unchecked")
+      List<String> collections = (List<String>) 
existsCheckResult.get("collections");
+      exists = collections != null && collections.contains(collection);
+    } catch (Exception exc) {
+      // just ignore it since we're only interested in a positive result here
+    }
+    return exists;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static boolean safeCheckCoreExists(String solrUrl, String coreName, 
String credentials) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl, credentials)) {
+      boolean wait = false;
+      final long startWaitAt = System.nanoTime();
+      do {
+        if (wait) {
+          final int clamPeriodForStatusPollMs = 1000;
+          Thread.sleep(clamPeriodForStatusPollMs);
+        }
+        NamedList<Object> existsCheckResult =
+            CoreAdminRequest.getStatus(coreName, solrClient).getResponse();
+        NamedList<Object> status = (NamedList<Object>) 
existsCheckResult.get("status");
+        NamedList<Object> coreStatus = (NamedList<Object>) 
status.get(coreName);
+        Map<String, Object> failureStatus =
+            (Map<String, Object>) existsCheckResult.get("initFailures");
+        String errorMsg = (String) failureStatus.get(coreName);
+        final boolean hasName = coreStatus != null && 
coreStatus.asMap().containsKey(NAME);
+        exists = hasName || errorMsg != null;
+        wait = hasName && errorMsg == null && 
"true".equals(coreStatus.get("isLoading"));
+      } while (wait && System.nanoTime() - startWaitAt < 
MAX_WAIT_FOR_CORE_LOAD_NANOS);
+    } catch (Exception exc) {
+      // just ignore it since we're only interested in a positive result here
+    }
+    return exists;
+  }
+
+  public static boolean isCloudMode(SolrClient solrClient) throws 
SolrServerException, IOException {
+    NamedList<Object> systemInfo =
+        solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, 
SYSTEM_INFO_PATH));
+    return "solrcloud".equals(systemInfo.get("mode"));
+  }
+
+  public static Path getConfigSetsDir(Path solrInstallDir) {
+    Path configSetsPath = Paths.get("server/solr/configsets/");
+    return solrInstallDir.resolve(configSetsPath);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java 
b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
index 1c71b7b8fee..714654829cc 100644
--- a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java
@@ -79,7 +79,7 @@ public class ClusterTool extends ToolBase {
 
     String propertyName = cli.getOptionValue(PROPERTY_OPTION);
     String propertyValue = cli.getOptionValue(VALUE_OPTION);
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     if (!ZkController.checkChrootPath(zkHost, true)) {
       throw new IllegalStateException(
diff --git a/solr/core/src/java/org/apache/solr/cli/CommonCLIOptions.java 
b/solr/core/src/java/org/apache/solr/cli/CommonCLIOptions.java
index 93f02c3e359..e6425abe1f3 100644
--- a/solr/core/src/java/org/apache/solr/cli/CommonCLIOptions.java
+++ b/solr/core/src/java/org/apache/solr/cli/CommonCLIOptions.java
@@ -47,7 +47,7 @@ public final class CommonCLIOptions {
           .argName("HOST")
           .desc(
               "Base Solr URL, which can be used to determine the zk-host if 
that's not known; defaults to: "
-                  + SolrCLI.getDefaultSolrUrl()
+                  + CLIUtils.getDefaultSolrUrl()
                   + '.')
           .build();
 
diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java 
b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java
index b55b939117b..264023640f7 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java
@@ -79,13 +79,13 @@ public class ConfigSetDownloadTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     String confName = cli.getOptionValue(CONF_NAME_OPTION);
     String confDir = cli.getOptionValue(CONF_DIR_OPTION);
 
     echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
       Path configSetPath = Paths.get(confDir);
       // we try to be nice about having the "conf" in the directory, and we 
create it if it's not
       // there.
diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java 
b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java
index b959245af2e..a9fc0631e11 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java
@@ -81,7 +81,7 @@ public class ConfigSetUploadTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     final String solrInstallDir = System.getProperty("solr.install.dir");
     Path solrInstallDirPath = Paths.get(solrInstallDir);
@@ -90,8 +90,8 @@ public class ConfigSetUploadTool extends ToolBase {
     String confDir = cli.getOptionValue(CONF_DIR_OPTION);
 
     echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
-      final Path configsetsDirPath = 
SolrCLI.getConfigSetsDir(solrInstallDirPath);
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
+      final Path configsetsDirPath = 
CLIUtils.getConfigSetsDir(solrInstallDirPath);
       Path confPath = ConfigSetService.getConfigsetPath(confDir, 
configsetsDirPath.toString());
 
       echo(
diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java 
b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
index fdd18f24d4b..e5e7c96791c 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
@@ -98,7 +98,7 @@ public class ConfigTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
+    String solrUrl = CLIUtils.normalizeSolrUrl(cli);
     String action = cli.getOptionValue(ACTION_OPTION, "set-property");
     String collection = cli.getOptionValue(COLLECTION_NAME_OPTION);
     String property = cli.getOptionValue(PROPERTY_OPTION);
@@ -127,7 +127,7 @@ public class ConfigTool extends ToolBase {
     echoIfVerbose(jsonBody);
 
     try (SolrClient solrClient =
-        SolrCLI.getSolrClient(solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
+        CLIUtils.getSolrClient(solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
       NamedList<Object> result = SolrCLI.postJsonToSolr(solrClient, 
updatePath, jsonBody);
       Integer statusCode = (Integer) result.findRecursive("responseHeader", 
"status");
       if (statusCode == 0) {
diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java 
b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
index 5fc57501d93..b1607af071b 100644
--- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
@@ -138,8 +138,8 @@ public class CreateTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
-      if (SolrCLI.isCloudMode(solrClient)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
+      if (CLIUtils.isCloudMode(solrClient)) {
         createCollection(cli);
       } else {
         createCore(cli, solrClient);
@@ -150,7 +150,7 @@ public class CreateTool extends ToolBase {
   protected void createCore(CommandLine cli, SolrClient solrClient) throws 
Exception {
     String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION);
     String solrUrl =
-        cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, 
SolrCLI.getDefaultSolrUrl());
+        cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, 
CLIUtils.getDefaultSolrUrl());
 
     final String solrInstallDir = System.getProperty("solr.install.dir");
     final String confDirName =
@@ -175,7 +175,7 @@ public class CreateTool extends ToolBase {
     // convert raw JSON into user-friendly output
     coreRootDirectory = (String) systemInfo.get("core_root");
 
-    if (SolrCLI.safeCheckCoreExists(
+    if (CLIUtils.safeCheckCoreExists(
         solrUrl, coreName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
       throw new IllegalArgumentException(
           "\nCore '"
@@ -224,9 +224,9 @@ public class CreateTool extends ToolBase {
             .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
             .withOptionalBasicAuthCredentials(
                 cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
     echoIfVerbose("Connecting to ZooKeeper at " + zkHost);
-    try (CloudSolrClient cloudSolrClient = 
SolrCLI.getCloudHttp2SolrClient(zkHost, builder)) {
+    try (CloudSolrClient cloudSolrClient = 
CLIUtils.getCloudHttp2SolrClient(zkHost, builder)) {
       cloudSolrClient.connect();
       createCollection(cloudSolrClient, cli);
     }
@@ -277,7 +277,7 @@ public class CreateTool extends ToolBase {
       }
 
       // TODO: This should be done using the configSet API
-      final Path configsetsDirPath = 
SolrCLI.getConfigSetsDir(solrInstallDirPath);
+      final Path configsetsDirPath = 
CLIUtils.getConfigSetsDir(solrInstallDirPath);
       ConfigSetService configSetService =
           new 
ZkConfigSetService(ZkStateReader.from(cloudSolrClient).getZkClient());
       Path confPath = ConfigSetService.getConfigsetPath(confDir, 
configsetsDirPath.toString());
@@ -294,7 +294,7 @@ public class CreateTool extends ToolBase {
     }
 
     // since creating a collection is a heavy-weight operation, check for 
existence first
-    if (SolrCLI.safeCheckCollectionExists(
+    if (CLIUtils.safeCheckCollectionExists(
         solrUrl, collectionName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
       throw new IllegalStateException(
           "\nCollection '"
@@ -339,7 +339,7 @@ public class CreateTool extends ToolBase {
   }
 
   private Path getFullConfDir(Path solrInstallDir, Path confDirName) {
-    return SolrCLI.getConfigSetsDir(solrInstallDir).resolve(confDirName);
+    return CLIUtils.getConfigSetsDir(solrInstallDir).resolve(confDirName);
   }
 
   private void ensureConfDirExists(Path solrInstallDir, Path confDirName) {
@@ -362,7 +362,7 @@ public class CreateTool extends ToolBase {
         && (confName.equals("") || confName.equals("_default"))) {
       final String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
       final String solrUrl =
-          cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, 
SolrCLI.getDefaultSolrUrl());
+          cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, 
CLIUtils.getDefaultSolrUrl());
       final String curlCommand =
           String.format(
               Locale.ROOT,
diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java 
b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
index 2c42f8dc030..7fc94cb85d3 100644
--- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
@@ -101,8 +101,8 @@ public class DeleteTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
-      if (SolrCLI.isCloudMode(solrClient)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
+      if (CLIUtils.isCloudMode(solrClient)) {
         deleteCollection(cli);
       } else {
         deleteCore(cli, solrClient);
@@ -119,8 +119,8 @@ public class DeleteTool extends ToolBase {
             .withOptionalBasicAuthCredentials(
                 cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION));
 
-    String zkHost = SolrCLI.getZkHost(cli);
-    try (CloudSolrClient cloudSolrClient = 
SolrCLI.getCloudHttp2SolrClient(zkHost, builder)) {
+    String zkHost = CLIUtils.getZkHost(cli);
+    try (CloudSolrClient cloudSolrClient = 
CLIUtils.getCloudHttp2SolrClient(zkHost, builder)) {
       echoIfVerbose("Connecting to ZooKeeper at " + zkHost);
       cloudSolrClient.connect();
       deleteCollection(cloudSolrClient, cli);
diff --git a/solr/core/src/java/org/apache/solr/cli/ExportTool.java 
b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
index 5018855e597..ee8e5a42c78 100644
--- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
@@ -284,7 +284,7 @@ public class ExportTool extends ToolBase {
         throw new IllegalArgumentException(
             "Must specify -c / --name parameter with --solr-url to post 
documents.");
       }
-      url = SolrCLI.normalizeSolrUrl(cli) + "/solr/" + 
cli.getOptionValue(COLLECTION_NAME_OPTION);
+      url = CLIUtils.normalizeSolrUrl(cli) + "/solr/" + 
cli.getOptionValue(COLLECTION_NAME_OPTION);
 
     } else {
       // think about support --zk-host someday.
@@ -664,7 +664,7 @@ public class ExportTool extends ToolBase {
 
       boolean exportDocsFromCore() throws IOException, SolrServerException {
 
-        try (SolrClient client = SolrCLI.getSolrClient(baseurl, credentials)) {
+        try (SolrClient client = CLIUtils.getSolrClient(baseurl, credentials)) 
{
           expectedDocs = getDocCount(replica.getCoreName(), client, query);
           QueryRequest request;
           ModifiableSolrParams params = new ModifiableSolrParams();
diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java 
b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
index 6ec08f77d6c..8a6b293e17a 100644
--- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java
@@ -89,12 +89,12 @@ public class HealthcheckTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
     if (zkHost == null) {
       CLIO.err("Healthcheck tool only works in Solr Cloud mode.");
       System.exit(1);
     }
-    try (CloudHttp2SolrClient cloudSolrClient = 
SolrCLI.getCloudHttp2SolrClient(zkHost)) {
+    try (CloudHttp2SolrClient cloudSolrClient = 
CLIUtils.getCloudHttp2SolrClient(zkHost)) {
       echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
       cloudSolrClient.connect();
       runCloudTool(cloudSolrClient, cli);
@@ -125,7 +125,7 @@ public class HealthcheckTool extends ToolBase {
     SolrQuery q = new SolrQuery("*:*");
     q.setRows(0);
     QueryResponse qr = cloudSolrClient.query(collection, q);
-    SolrCLI.checkCodeForAuthError(qr.getStatus());
+    CLIUtils.checkCodeForAuthError(qr.getStatus());
     String collErr = null;
     long docCount = -1;
     try {
@@ -169,12 +169,12 @@ public class HealthcheckTool extends ToolBase {
           q.setRows(0);
           q.set(DISTRIB, "false");
           try (var solrClientForCollection =
-              SolrCLI.getSolrClient(
+              CLIUtils.getSolrClient(
                   coreUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
             qr = solrClientForCollection.query(q);
             numDocs = qr.getResults().getNumFound();
             try (var solrClient =
-                SolrCLI.getSolrClient(
+                CLIUtils.getSolrClient(
                     replicaCoreProps.getBaseUrl(),
                     cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
               NamedList<Object> systemInfo =
@@ -192,7 +192,7 @@ public class HealthcheckTool extends ToolBase {
           } catch (Exception exc) {
             log.error("ERROR: {} when trying to reach: {}", exc, coreUrl);
 
-            if (SolrCLI.checkCommunicationError(exc)) {
+            if (CLIUtils.checkCommunicationError(exc)) {
               replicaStatus = Replica.State.DOWN.toString();
             } else {
               replicaStatus = "error: " + exc;
diff --git a/solr/core/src/java/org/apache/solr/cli/LinkConfigTool.java 
b/solr/core/src/java/org/apache/solr/cli/LinkConfigTool.java
index 2261b96474d..619e1d82e5c 100644
--- a/solr/core/src/java/org/apache/solr/cli/LinkConfigTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/LinkConfigTool.java
@@ -78,7 +78,7 @@ public class LinkConfigTool extends ToolBase {
 
     String collection = cli.getOptionValue(COLLECTION_NAME_OPTION);
     String confName = cli.getOptionValue(CONF_NAME_OPTION);
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     try (SolrZkClient zkClient =
         new SolrZkClient.Builder()
diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java 
b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
index cb8bc6493ad..b71c86896cb 100644
--- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
@@ -115,8 +115,8 @@ public class PackageTool extends ToolBase {
               + "don't print stack traces, hence special treatment is needed 
here.")
   public void runImpl(CommandLine cli) throws Exception {
     try {
-      String solrUrl = SolrCLI.normalizeSolrUrl(cli);
-      String zkHost = SolrCLI.getZkHost(cli);
+      String solrUrl = CLIUtils.normalizeSolrUrl(cli);
+      String zkHost = CLIUtils.getZkHost(cli);
       if (zkHost == null) {
         throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs 
only in SolrCloud");
       }
@@ -125,7 +125,7 @@ public class PackageTool extends ToolBase {
 
       String cmd = cli.getArgs()[0];
 
-      try (SolrClient solrClient = SolrCLI.getSolrClient(cli, true)) {
+      try (SolrClient solrClient = CLIUtils.getSolrClient(cli, true)) {
         packageManager = new PackageManager(solrClient, solrUrl, zkHost);
         try {
           repositoryManager = new RepositoryManager(solrClient, 
packageManager);
diff --git a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java 
b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
index e1efeb0b920..c14d9c216db 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java
@@ -93,7 +93,7 @@ public class PostLogsTool extends ToolBase {
   public void runImpl(CommandLine cli) throws Exception {
     String url = null;
     if (cli.hasOption(CommonCLIOptions.SOLR_URL_OPTION)) {
-      url = SolrCLI.normalizeSolrUrl(cli) + "/solr/" + 
cli.getOptionValue(COLLECTION_NAME_OPTION);
+      url = CLIUtils.normalizeSolrUrl(cli) + "/solr/" + 
cli.getOptionValue(COLLECTION_NAME_OPTION);
 
     } else {
       // Could be required arg, but maybe we want to support --zk-host option 
too?
diff --git a/solr/core/src/java/org/apache/solr/cli/PostTool.java 
b/solr/core/src/java/org/apache/solr/cli/PostTool.java
index 0e50e619043..f6bc4b811f0 100644
--- a/solr/core/src/java/org/apache/solr/cli/PostTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java
@@ -279,7 +279,7 @@ public class PostTool extends ToolBase {
     solrUpdateUrl = null;
     if (cli.hasOption(CommonCLIOptions.SOLR_URL_OPTION)) {
       String url =
-          SolrCLI.normalizeSolrUrl(cli)
+          CLIUtils.normalizeSolrUrl(cli)
               + "/solr/"
               + cli.getOptionValue(COLLECTION_NAME_OPTION)
               + "/update";
@@ -287,7 +287,7 @@ public class PostTool extends ToolBase {
 
     } else {
       String url =
-          SolrCLI.getDefaultSolrUrl()
+          CLIUtils.getDefaultSolrUrl()
               + "/solr/"
               + cli.getOptionValue(COLLECTION_NAME_OPTION)
               + "/update";
@@ -765,7 +765,7 @@ public class PostTool extends ToolBase {
     info("COMMITting Solr index changes to " + solrUpdateUrl + "...");
     String url = solrUpdateUrl.toString();
     url = url.substring(0, url.lastIndexOf("/update"));
-    try (final SolrClient client = SolrCLI.getSolrClient(url, credentials)) {
+    try (final SolrClient client = CLIUtils.getSolrClient(url, credentials)) {
       client.commit();
     }
   }
@@ -775,7 +775,7 @@ public class PostTool extends ToolBase {
     info("Performing an OPTIMIZE to " + solrUpdateUrl + "...");
     String url = solrUpdateUrl.toString();
     url = url.substring(0, url.lastIndexOf("/update"));
-    try (final SolrClient client = SolrCLI.getSolrClient(url, credentials)) {
+    try (final SolrClient client = CLIUtils.getSolrClient(url, credentials)) {
       client.optimize();
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java 
b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
index 99307cf74f0..fff3c04f5c5 100644
--- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
@@ -286,7 +286,7 @@ public class RunExampleTool extends ToolBase {
     boolean alreadyExists = false;
     boolean cloudMode = nodeStatus.get("cloud") != null;
     if (cloudMode) {
-      if (SolrCLI.safeCheckCollectionExists(
+      if (CLIUtils.safeCheckCollectionExists(
           solrUrl, collectionName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
         alreadyExists = true;
         echo(
@@ -296,7 +296,7 @@ public class RunExampleTool extends ToolBase {
       }
     } else {
       String coreName = collectionName;
-      if (SolrCLI.safeCheckCoreExists(
+      if (CLIUtils.safeCheckCoreExists(
           solrUrl, coreName, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) {
         alreadyExists = true;
         echo(
@@ -800,7 +800,7 @@ public class RunExampleTool extends ToolBase {
 
         // Test for existence and then prompt to either create another 
collection or skip the
         // creation step
-        if (SolrCLI.safeCheckCollectionExists(solrUrl, credentials, 
collectionName)) {
+        if (CLIUtils.safeCheckCollectionExists(solrUrl, credentials, 
collectionName)) {
           echo("\nCollection '" + collectionName + "' already exists!");
           int oneOrTwo =
               promptForInt(
@@ -856,7 +856,7 @@ public class RunExampleTool extends ToolBase {
       }
     } else {
       // must verify if default collection exists
-      if (SolrCLI.safeCheckCollectionExists(solrUrl, collectionName, 
credentials)) {
+      if (CLIUtils.safeCheckCollectionExists(solrUrl, collectionName, 
credentials)) {
         echo(
             "\nCollection '"
                 + collectionName
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
index 236a7416832..49cce02a15c 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java
@@ -71,7 +71,7 @@ public class SnapshotCreateTool extends ToolBase {
   public void runImpl(CommandLine cli) throws Exception {
     String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
     String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
       createSnapshot(solrClient, collectionName, snapshotName);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
index 53fa8ad002a..c9dc90abab7 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java
@@ -71,7 +71,7 @@ public class SnapshotDeleteTool extends ToolBase {
   public void runImpl(CommandLine cli) throws Exception {
     String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
     String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
       deleteSnapshot(solrClient, collectionName, snapshotName);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
index b76f36a7420..dcae8619f3e 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java
@@ -84,7 +84,7 @@ public class SnapshotDescribeTool extends ToolBase {
   public void runImpl(CommandLine cli) throws Exception {
     String snapshotName = cli.getOptionValue(SNAPSHOT_NAME_OPTION);
     String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
       describeSnapshot(solrClient, collectionName, snapshotName);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
index a51e91badb3..6b8cf3a45d0 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java
@@ -106,7 +106,7 @@ public class SnapshotExportTool extends ToolBase {
     Optional<String> backupRepo = 
Optional.ofNullable(cli.getOptionValue(BACKUP_REPO_NAME_OPTION));
     Optional<String> asyncReqId = 
Optional.ofNullable(cli.getOptionValue(ASYNC_ID_OPTION));
 
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
       exportSnapshot(solrClient, collectionName, snapshotName, destDir, 
backupRepo, asyncReqId);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java 
b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
index 87219ce6021..4b952199e1b 100644
--- a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java
@@ -62,7 +62,7 @@ public class SnapshotListTool extends ToolBase {
   @Override
   public void runImpl(CommandLine cli) throws Exception {
     String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION);
-    try (var solrClient = SolrCLI.getSolrClient(cli)) {
+    try (var solrClient = CLIUtils.getSolrClient(cli)) {
       listSnapshots(solrClient, collectionName);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java 
b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
index 4714c43c99c..32483720d17 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -16,32 +16,19 @@
  */
 package org.apache.solr.cli;
 
-import static org.apache.solr.common.SolrException.ErrorCode.FORBIDDEN;
-import static org.apache.solr.common.SolrException.ErrorCode.UNAUTHORIZED;
-import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.common.params.CommonParams.SYSTEM_INFO_PATH;
-
 import com.google.common.annotations.VisibleForTesting;
 import java.io.File;
-import java.io.IOException;
 import java.lang.invoke.MethodHandles;
-import java.net.SocketException;
 import java.net.URI;
 import java.net.URL;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import org.apache.commons.cli.CommandLine;
@@ -51,22 +38,8 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
-import org.apache.solr.client.solrj.impl.SolrZkClientTimeout;
-import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
-import org.apache.solr.client.solrj.request.CoreAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.cloud.SolrZkClient;
-import org.apache.solr.common.cloud.ZkStateReader;
-import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.ContentStreamBase;
-import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.util.configuration.SSLConfigurationsFactory;
 import org.slf4j.Logger;
@@ -75,13 +48,6 @@ import org.slf4j.LoggerFactory;
 /** Command-line utility for working with Solr. */
 public class SolrCLI implements CLIO {
 
-  public static String RED = "\u001B[31m";
-  public static String GREEN = "\u001B[32m";
-  public static String YELLOW = "\u001B[33m";
-
-  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
-      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
-
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   public static void exit(int exitStatus) {
@@ -188,14 +154,6 @@ public class SolrCLI implements CLIO {
     return cli;
   }
 
-  public static String getDefaultSolrUrl() {
-    // note that ENV_VAR syntax (and the env vars too) are mapped to env.var 
sys props
-    String scheme = EnvUtils.getProperty("solr.url.scheme", "http");
-    String host = EnvUtils.getProperty("solr.tool.host", "localhost");
-    String port = EnvUtils.getProperty("jetty.port", "8983"); // from 
SOLR_PORT env
-    return String.format(Locale.ROOT, "%s://%s:%s", 
scheme.toLowerCase(Locale.ROOT), host, port);
-  }
-
   protected static void checkSslStoreSysProp(String solrInstallDir, String 
key) {
     String sysProp = "javax.net.ssl." + key;
     String keyStore = System.getProperty(sysProp);
@@ -408,70 +366,6 @@ public class SolrCLI implements CLIO {
     return classes;
   }
 
-  /**
-   * Determine if a request to Solr failed due to a communication error, which 
is generally
-   * retry-able.
-   */
-  public static boolean checkCommunicationError(Exception exc) {
-    Throwable rootCause = SolrException.getRootCause(exc);
-    return (rootCause instanceof SolrServerException || rootCause instanceof 
SocketException);
-  }
-
-  public static void checkCodeForAuthError(int code) {
-    if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
-      throw new SolrException(
-          SolrException.ErrorCode.getErrorCode(code),
-          "Solr requires authentication for request. Please supply valid 
credentials. HTTP code="
-              + code);
-    }
-  }
-
-  public static boolean exceptionIsAuthRelated(Exception exc) {
-    return (exc instanceof SolrException
-        && Arrays.asList(UNAUTHORIZED.code, 
FORBIDDEN.code).contains(((SolrException) exc).code()));
-  }
-
-  public static SolrClient getSolrClient(String solrUrl, String credentials, 
boolean barePath) {
-    // today we require all urls to end in /solr, however in the future we 
will need to support the
-    // /api url end point instead.   Eventually we want to have this method 
always
-    // return a bare url, and then individual calls decide if they are /solr 
or /api
-    // The /solr/ check is because sometimes a full url is passed in, like
-    // http://localhost:8983/solr/films_shard1_replica_n1/.
-    if (!barePath && !solrUrl.endsWith("/solr") && 
!solrUrl.contains("/solr/")) {
-      solrUrl = solrUrl + "/solr";
-    }
-    Http2SolrClient.Builder builder =
-        new Http2SolrClient.Builder(solrUrl)
-            .withMaxConnectionsPerHost(32)
-            .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS)
-            .withOptionalBasicAuthCredentials(credentials);
-
-    return builder.build();
-  }
-
-  /**
-   * Helper method for all the places where we assume a /solr on the url.
-   *
-   * @param solrUrl The solr url that you want the client for
-   * @param credentials The username:password for basic auth.
-   * @return The SolrClient
-   */
-  public static SolrClient getSolrClient(String solrUrl, String credentials) {
-    return getSolrClient(solrUrl, credentials, false);
-  }
-
-  public static SolrClient getSolrClient(CommandLine cli, boolean barePath) 
throws Exception {
-    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
-    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
-    return getSolrClient(solrUrl, credentials, barePath);
-  }
-
-  public static SolrClient getSolrClient(CommandLine cli) throws Exception {
-    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
-    String credentials = 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
-    return getSolrClient(solrUrl, credentials, false);
-  }
-
   private static final String JSON_CONTENT_TYPE = "application/json";
 
   public static NamedList<Object> postJsonToSolr(
@@ -540,204 +434,18 @@ public class SolrCLI implements CLIO {
     print("For more help on how to use Solr, head to 
https://solr.apache.org/";);
   }
 
-  /**
-   * Strips off the end of solrUrl any /solr when a legacy solrUrl like 
http://localhost:8983/solr
-   * is used, and warns those users. In the future we'll have urls ending with 
/api as well.
-   *
-   * @param solrUrl The user supplied url to Solr.
-   * @return the solrUrl in the format that Solr expects to see internally.
-   */
-  public static String normalizeSolrUrl(String solrUrl) {
-    return normalizeSolrUrl(solrUrl, true);
-  }
-
-  /**
-   * Strips off the end of solrUrl any /solr when a legacy solrUrl like 
http://localhost:8983/solr
-   * is used, and optionally logs a warning. In the future we'll have urls 
ending with /api as well.
-   *
-   * @param solrUrl The user supplied url to Solr.
-   * @param logUrlFormatWarning If a warning message should be logged about 
the url format
-   * @return the solrUrl in the format that Solr expects to see internally.
-   */
-  public static String normalizeSolrUrl(String solrUrl, boolean 
logUrlFormatWarning) {
-    if (solrUrl != null) {
-      URI uri = URI.create(solrUrl);
-      String urlPath = uri.getRawPath();
-      if (urlPath != null && urlPath.contains("/solr")) {
-        String newSolrUrl =
-            uri.resolve(urlPath.substring(0, urlPath.lastIndexOf("/solr") + 
1)).toString();
-        if (logUrlFormatWarning) {
-          CLIO.err(
-              "WARNING: URLs provided to this tool needn't include Solr's 
context-root (e.g. \"/solr\"). Such URLs are deprecated and support for them 
will be removed in a future release. Correcting from ["
-                  + solrUrl
-                  + "] to ["
-                  + newSolrUrl
-                  + "].");
-        }
-        solrUrl = newSolrUrl;
-      }
-      if (solrUrl.endsWith("/")) {
-        solrUrl = solrUrl.substring(0, solrUrl.length() - 1);
-      }
-    }
-    return solrUrl;
-  }
-
-  /**
-   * Get the base URL of a live Solr instance from either the --solr-url 
command-line option or from
-   * ZooKeeper.
-   */
-  public static String normalizeSolrUrl(CommandLine cli) throws Exception {
-    String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION);
-    if (solrUrl == null) {
-      String zkHost = cli.getOptionValue(CommonCLIOptions.ZK_HOST_OPTION);
-      if (zkHost == null) {
-        solrUrl = SolrCLI.getDefaultSolrUrl();
-        CLIO.err(
-            "Neither --zk-host or --solr-url parameters provided so assuming 
solr url is "
-                + solrUrl
-                + ".");
-      } else {
-        try (CloudSolrClient cloudSolrClient = 
getCloudHttp2SolrClient(zkHost)) {
-          cloudSolrClient.connect();
-          Set<String> liveNodes = 
cloudSolrClient.getClusterState().getLiveNodes();
-          if (liveNodes.isEmpty())
-            throw new IllegalStateException(
-                "No live nodes found! Cannot determine 'solrUrl' from 
ZooKeeper: " + zkHost);
-
-          String firstLiveNode = liveNodes.iterator().next();
-          solrUrl = 
ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode);
-          solrUrl = normalizeSolrUrl(solrUrl, false);
-        }
-      }
-    }
-    solrUrl = normalizeSolrUrl(solrUrl);
-    return solrUrl;
-  }
-
-  /**
-   * Get the ZooKeeper connection string from either the zk-host command-line 
option or by looking
-   * it up from a running Solr instance based on the solr-url option.
-   */
-  public static String getZkHost(CommandLine cli) throws Exception {
-
-    String zkHost = cli.getOptionValue(CommonCLIOptions.ZK_HOST_OPTION);
-    if (zkHost != null && !zkHost.isBlank()) {
-      return zkHost;
-    }
-
-    try (SolrClient solrClient = getSolrClient(cli)) {
-      // hit Solr to get system info
-      NamedList<Object> systemInfo =
-          solrClient.request(
-              new GenericSolrRequest(SolrRequest.METHOD.GET, 
CommonParams.SYSTEM_INFO_PATH));
-
-      // convert raw JSON into user-friendly output
-      StatusTool statusTool = new StatusTool();
-      Map<String, Object> status = statusTool.reportStatus(systemInfo, 
solrClient);
-      @SuppressWarnings("unchecked")
-      Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
-      if (cloud != null) {
-        String zookeeper = (String) cloud.get("ZooKeeper");
-        if (zookeeper.endsWith("(embedded)")) {
-          zookeeper = zookeeper.substring(0, zookeeper.length() - 
"(embedded)".length());
-        }
-        zkHost = zookeeper;
-      }
-    }
-
-    return zkHost;
-  }
-
-  public static SolrZkClient getSolrZkClient(CommandLine cli, String zkHost) 
throws Exception {
-    if (zkHost == null) {
-      throw new IllegalStateException(
-          "Solr at "
-              + cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION)
-              + " is running in standalone server mode, this command can only 
be used when running in SolrCloud mode.\n");
-    }
-    return new SolrZkClient.Builder()
-        .withUrl(zkHost)
-        .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, 
TimeUnit.MILLISECONDS)
-        .build();
-  }
-
-  public static CloudHttp2SolrClient getCloudHttp2SolrClient(String zkHost) {
-    return getCloudHttp2SolrClient(zkHost, null);
-  }
-
-  public static CloudHttp2SolrClient getCloudHttp2SolrClient(
-      String zkHost, Http2SolrClient.Builder builder) {
-    return new CloudHttp2SolrClient.Builder(Collections.singletonList(zkHost), 
Optional.empty())
-        .withInternalClientBuilder(builder)
-        .build();
-  }
-
-  public static boolean safeCheckCollectionExists(
-      String solrUrl, String collection, String credentials) {
-    boolean exists = false;
-    try (var solrClient = getSolrClient(solrUrl, credentials)) {
-      NamedList<Object> existsCheckResult = solrClient.request(new 
CollectionAdminRequest.List());
-      @SuppressWarnings("unchecked")
-      List<String> collections = (List<String>) 
existsCheckResult.get("collections");
-      exists = collections != null && collections.contains(collection);
-    } catch (Exception exc) {
-      // just ignore it since we're only interested in a positive result here
-    }
-    return exists;
-  }
-
-  @SuppressWarnings("unchecked")
-  public static boolean safeCheckCoreExists(String solrUrl, String coreName, 
String credentials) {
-    boolean exists = false;
-    try (var solrClient = getSolrClient(solrUrl, credentials)) {
-      boolean wait = false;
-      final long startWaitAt = System.nanoTime();
-      do {
-        if (wait) {
-          final int clamPeriodForStatusPollMs = 1000;
-          Thread.sleep(clamPeriodForStatusPollMs);
-        }
-        NamedList<Object> existsCheckResult =
-            CoreAdminRequest.getStatus(coreName, solrClient).getResponse();
-        NamedList<Object> status = (NamedList<Object>) 
existsCheckResult.get("status");
-        NamedList<Object> coreStatus = (NamedList<Object>) 
status.get(coreName);
-        Map<String, Object> failureStatus =
-            (Map<String, Object>) existsCheckResult.get("initFailures");
-        String errorMsg = (String) failureStatus.get(coreName);
-        final boolean hasName = coreStatus != null && 
coreStatus.asMap().containsKey(NAME);
-        exists = hasName || errorMsg != null;
-        wait = hasName && errorMsg == null && 
"true".equals(coreStatus.get("isLoading"));
-      } while (wait && System.nanoTime() - startWaitAt < 
MAX_WAIT_FOR_CORE_LOAD_NANOS);
-    } catch (Exception exc) {
-      // just ignore it since we're only interested in a positive result here
-    }
-    return exists;
-  }
-
-  public static boolean isCloudMode(SolrClient solrClient) throws 
SolrServerException, IOException {
-    NamedList<Object> systemInfo =
-        solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, 
SYSTEM_INFO_PATH));
-    return "solrcloud".equals(systemInfo.get("mode"));
-  }
-
-  public static Path getConfigSetsDir(Path solrInstallDir) {
-    Path configSetsPath = Paths.get("server/solr/configsets/");
-    return solrInstallDir.resolve(configSetsPath);
-  }
-
   public static void print(Object message) {
     print(null, message);
   }
 
   /** Console print using green color */
   public static void printGreen(Object message) {
-    print(GREEN, message);
+    print(CLIUtils.GREEN, message);
   }
 
   /** Console print using red color */
   public static void printRed(Object message) {
-    print(RED, message);
+    print(CLIUtils.RED, message);
   }
 
   public static void print(String color, Object message) {
diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java 
b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
index dcf0200e511..45713fbc085 100644
--- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
@@ -18,8 +18,6 @@
 package org.apache.solr.cli;
 
 import java.io.PrintStream;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -188,25 +186,15 @@ public class StatusTool extends ToolBase {
     CLIO.out("");
   }
 
-  private Integer portFromUrl(String solrUrl) {
+  public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int 
maxWaitSecs)
+      throws Exception {
+    int solrPort = -1;
     try {
-      URI uri = new URI(solrUrl);
-      int port = uri.getPort();
-      if (port == -1) {
-        return uri.getScheme().equals("https") ? 443 : 80;
-      } else {
-        return port;
-      }
-    } catch (URISyntaxException e) {
+      solrPort = CLIUtils.portFromUrl(solrUrl);
+    } catch (Exception e) {
       CLIO.err("Invalid URL provided, does not contain port");
-      System.exit(1);
-      return null;
+      SolrCLI.exit(1);
     }
-  }
-
-  public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int 
maxWaitSecs)
-      throws Exception {
-    int solrPort = portFromUrl(solrUrl);
     echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on 
port " + solrPort);
     boolean solrUp = waitForSolrUp(solrUrl, cli, maxWaitSecs);
     if (solrUp) {
@@ -268,10 +256,10 @@ public class StatusTool extends ToolBase {
           .write(getStatus(solrUrl, 
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)));
       return arr.toString();
     } catch (Exception exc) {
-      if (SolrCLI.exceptionIsAuthRelated(exc)) {
+      if (CLIUtils.exceptionIsAuthRelated(exc)) {
         throw exc;
       }
-      if (SolrCLI.checkCommunicationError(exc)) {
+      if (CLIUtils.checkCommunicationError(exc)) {
         // this is not actually an error from the tool as it's ok if Solr is 
not online.
         return null;
       } else {
@@ -289,7 +277,7 @@ public class StatusTool extends ToolBase {
       try {
         return getStatus(solrUrl, credentials);
       } catch (Exception exc) {
-        if (SolrCLI.exceptionIsAuthRelated(exc)) {
+        if (CLIUtils.exceptionIsAuthRelated(exc)) {
           throw exc;
         }
         try {
@@ -308,7 +296,7 @@ public class StatusTool extends ToolBase {
   }
 
   public Map<String, Object> getStatus(String solrUrl, String credentials) 
throws Exception {
-    try (var solrClient = SolrCLI.getSolrClient(solrUrl, credentials)) {
+    try (var solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) {
       return getStatus(solrClient);
     }
   }
diff --git a/solr/core/src/java/org/apache/solr/cli/StreamTool.java 
b/solr/core/src/java/org/apache/solr/cli/StreamTool.java
index 9c0392ec71b..512b678da0f 100644
--- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java
@@ -253,7 +253,7 @@ public class StreamTool extends ToolBase {
    *     locally.
    */
   private PushBackStream doLocalMode(CommandLine cli, String expr) throws 
Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     echoIfVerbose("Connecting to ZooKeeper at " + zkHost);
     solrClientCache.getCloudSolrClient(zkHost);
@@ -306,7 +306,7 @@ public class StreamTool extends ToolBase {
    */
   private PushBackStream doRemoteMode(CommandLine cli, String expr) throws 
Exception {
 
-    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
+    String solrUrl = CLIUtils.normalizeSolrUrl(cli);
     if (!cli.hasOption("name")) {
       throw new IllegalStateException(
           "You must provide --name COLLECTION with --worker solr parameter.");
diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java 
b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java
index 996ada0a213..bf54152d2ac 100644
--- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java
@@ -59,7 +59,7 @@ public class UpdateACLTool extends ToolBase {
   @Override
   public void runImpl(CommandLine cli) throws Exception {
 
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
     String path = cli.getArgs()[0];
 
     if (!ZkController.checkChrootPath(zkHost, true)) {
diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java 
b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java
index b2045ef3541..7f901e67cf2 100644
--- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java
@@ -124,7 +124,7 @@ public class ZkCpTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
     String src = cli.getArgs()[0];
diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java 
b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java
index c8f1f55b266..57bb45d4e40 100644
--- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java
@@ -57,10 +57,10 @@ public class ZkLsTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
     String znode = cli.getArgs()[0];
 
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
       echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
 
       boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION);
diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java 
b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java
index f2dc3703835..ace32ad5ce7 100644
--- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java
@@ -76,11 +76,11 @@ public class ZkMkrootTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
     String znode = cli.getArgs()[0];
     boolean failOnExists = cli.hasOption(FAIL_ON_EXISTS_OPTION);
 
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
       echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
 
       echo("Creating ZooKeeper path " + znode + " on ZooKeeper at " + zkHost);
diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java 
b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java
index 41ca7d78822..dba47d5b824 100644
--- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java
@@ -74,9 +74,9 @@ public class ZkMvTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
       echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
       String src = cli.getArgs()[0];
       String dst = cli.getArgs()[1];
diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java 
b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java
index cdb2e600334..777ad6035a5 100644
--- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java
@@ -58,7 +58,7 @@ public class ZkRmTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String zkHost = SolrCLI.getZkHost(cli);
+    String zkHost = CLIUtils.getZkHost(cli);
 
     String target = cli.getArgs()[0];
     boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION);
@@ -71,7 +71,7 @@ public class ZkRmTool extends ToolBase {
       throw new SolrServerException("You may not remove the root ZK node 
('/')!");
     }
     echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ...");
-    try (SolrZkClient zkClient = SolrCLI.getSolrZkClient(cli, zkHost)) {
+    try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
       if (!recursive && zkClient.getChildren(znode, null, true).size() != 0) {
         throw new SolrServerException(
             "ZooKeeper node " + znode + " has children and recursive has NOT 
been specified.");
diff --git 
a/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java 
b/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
index a751d20472f..1d69a9c429d 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
@@ -40,6 +40,7 @@ import java.util.Scanner;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.solr.cli.CLIUtils;
 import org.apache.solr.cli.SolrCLI;
 import org.apache.solr.client.api.util.SolrVersion;
 import org.apache.solr.client.solrj.SolrClient;
@@ -680,7 +681,7 @@ public class PackageManager implements Closeable {
     boolean shouldExecute = true;
     if (!noprompt) { // show a prompt asking user to execute the setup command 
for the plugin
       PackageUtils.print(
-          SolrCLI.YELLOW,
+          CLIUtils.YELLOW,
           "Execute this command. (If you choose no, you can manually 
deploy/undeploy this plugin later) (y/n): ");
       try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)) {
         String userInput = scanner.next();
@@ -954,7 +955,7 @@ public class PackageManager implements Closeable {
             shouldInstallClusterPlugins,
             parameters);
     PackageUtils.print(
-        res ? SolrCLI.GREEN : SolrCLI.RED, res ? "Deployment successful" : 
"Deployment failed");
+        res ? CLIUtils.GREEN : CLIUtils.RED, res ? "Deployment successful" : 
"Deployment failed");
   }
 
   /** Undeploys a package from given collections. */
diff --git 
a/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java 
b/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
index 26b294b4906..1d1270a2b84 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/PackageUtils.java
@@ -33,7 +33,7 @@ import java.util.Map;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import org.apache.lucene.util.SuppressForbidden;
-import org.apache.solr.cli.SolrCLI;
+import org.apache.solr.cli.CLIUtils;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -236,7 +236,7 @@ public class PackageUtils {
 
   /** Console print using green color */
   public static void formatGreen(StringBuilder sb, Object message) {
-    format(sb, SolrCLI.GREEN, message);
+    format(sb, CLIUtils.GREEN, message);
   }
 
   public static void format(StringBuilder sb, Object message) {
diff --git a/solr/core/src/test/org/apache/solr/cli/CLIUtilsTest.java 
b/solr/core/src/test/org/apache/solr/cli/CLIUtilsTest.java
new file mode 100644
index 00000000000..928266de7c0
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/CLIUtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.cli;
+
+import java.net.SocketException;
+import java.net.URISyntaxException;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
+import org.junit.Test;
+
+public class CLIUtilsTest extends SolrCloudTestCase {
+
+  @Test
+  public void testDefaultSolrUrlWithNoProperties() {
+    System.clearProperty("solr.url.scheme");
+    System.clearProperty("solr.tool.host");
+    System.clearProperty("jetty.port");
+    assertEquals(
+        "Default Solr URL should match with no properties set.",
+        "http://localhost:8983";,
+        CLIUtils.getDefaultSolrUrl());
+  }
+
+  @Test
+  public void testDefaultSolrUrlWithProperties() {
+    System.setProperty("solr.url.scheme", "https");
+    System.setProperty("solr.tool.host", "other.local");
+    System.setProperty("jetty.port", "1234");
+    assertEquals(
+        "Default Solr URL should match with custom properties set.",
+        "https://other.local:1234";,
+        CLIUtils.getDefaultSolrUrl());
+  }
+
+  @Test
+  public void testCommunicationErrors() {
+    // communication errors
+    Exception serverException = new Exception(new SolrServerException(""));
+    assertTrue(
+        "SolrServerException should be communication error",
+        CLIUtils.checkCommunicationError(serverException));
+
+    Exception socketException = new RuntimeException(new Exception(new 
SocketException()));
+    assertTrue(
+        "SocketException should be communication error",
+        CLIUtils.checkCommunicationError(socketException));
+
+    // TODO See if this should be a communication error or not
+    //    Exception parentException = new SolrServerException(new Exception());
+    //    assertTrue(
+    //        "SolrServerException with different root cause should be 
communication error",
+    //        CLIUtils.checkCommunicationError(parentException));
+
+    Exception rootException = new SolrServerException("");
+    assertTrue(
+        "SolrServerException with no cause should be communication error",
+        CLIUtils.checkCommunicationError(rootException));
+
+    // non-communication errors
+    Exception exception1 = new NullPointerException();
+    assertFalse(
+        "NullPointerException should not be communication error",
+        CLIUtils.checkCommunicationError(exception1));
+
+    Exception exception2 = new RuntimeException(new Exception());
+    assertFalse(
+        "Exception should not be communication error",
+        CLIUtils.checkCommunicationError(exception2));
+  }
+
+  @Test
+  public void testCodeForAuthError() throws SolrException {
+    // auth errors
+    assertThrows(
+        "Forbidden (403) should throw SolrException",
+        SolrException.class,
+        () -> 
CLIUtils.checkCodeForAuthError(SolrException.ErrorCode.FORBIDDEN.code));
+    assertThrows(
+        "Unauthorized (401) should throw SolrException",
+        SolrException.class,
+        () -> 
CLIUtils.checkCodeForAuthError(SolrException.ErrorCode.UNAUTHORIZED.code));
+
+    // non auth errors
+    CLIUtils.checkCodeForAuthError(SolrException.ErrorCode.BAD_REQUEST.code);
+    CLIUtils.checkCodeForAuthError(SolrException.ErrorCode.CONFLICT.code);
+    CLIUtils.checkCodeForAuthError(SolrException.ErrorCode.SERVER_ERROR.code);
+    CLIUtils.checkCodeForAuthError(0); // Unknown
+    CLIUtils.checkCodeForAuthError(200); // HTTP OK
+  }
+
+  @Test
+  public void testResolveSolrUrl() {
+    assertEquals(CLIUtils.normalizeSolrUrl("http://localhost:8983/solr";), 
"http://localhost:8983";);
+    assertEquals(CLIUtils.normalizeSolrUrl("http://localhost:8983/solr/";), 
"http://localhost:8983";);
+    assertEquals(CLIUtils.normalizeSolrUrl("http://localhost:8983/";), 
"http://localhost:8983";);
+    assertEquals(CLIUtils.normalizeSolrUrl("http://localhost:8983";), 
"http://localhost:8983";);
+    assertEquals(
+        CLIUtils.normalizeSolrUrl("http://localhost:8983/solr/";, false), 
"http://localhost:8983";);
+  }
+
+  @Test
+  public void testPortExtraction() throws URISyntaxException {
+    assertEquals(
+        "Should extract explicit port from valid URL",
+        8983,
+        CLIUtils.portFromUrl("http://localhost:8983";));
+
+    assertEquals(
+        "Should extract explicit port from valid URL with trailing slash",
+        1234,
+        CLIUtils.portFromUrl("http://localhost:1234/";));
+
+    assertEquals(
+        "Should extract implicit HTTP port (80)", 80, 
CLIUtils.portFromUrl("http://localhost";));
+
+    assertEquals(
+        "Should extract implicit HTTPS port (443)", 443, 
CLIUtils.portFromUrl("https://localhost";));
+
+    // TODO See if we could be more lenient and fallback to defaults instead.
+    assertThrows(
+        "Should throw NullpointerException if no scheme provided",
+        NullPointerException.class,
+        () -> CLIUtils.portFromUrl("localhost"));
+
+    // Note that a bunch of invalid URIs like "http::example.com", 
"http:/example.com" and
+    // "//example.com" are not throwing URISyntaxException. This however is an 
issue of
+    // java.lang.URI, which is very lenient.
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java 
b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
index 4b5290a1ff7..d045ae35501 100644
--- a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
+++ b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
@@ -20,15 +20,6 @@ import org.apache.solr.SolrTestCase;
 import org.junit.Test;
 
 public class SolrCLITest extends SolrTestCase {
-  @Test
-  public void testResolveSolrUrl() {
-    assertEquals(SolrCLI.normalizeSolrUrl("http://localhost:8983/solr";), 
"http://localhost:8983";);
-    assertEquals(SolrCLI.normalizeSolrUrl("http://localhost:8983/solr/";), 
"http://localhost:8983";);
-    assertEquals(SolrCLI.normalizeSolrUrl("http://localhost:8983/";), 
"http://localhost:8983";);
-    assertEquals(SolrCLI.normalizeSolrUrl("http://localhost:8983";), 
"http://localhost:8983";);
-    assertEquals(
-        SolrCLI.normalizeSolrUrl("http://localhost:8983/solr/";, false), 
"http://localhost:8983";);
-  }
 
   @Test
   public void testUptime() {
diff --git a/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java 
b/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java
index 6c0522fd937..da279a12301 100644
--- a/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java
+++ b/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java
@@ -506,7 +506,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 {
 
     // verify Solr is running on the expected port and verify the collection 
exists
     String solrUrl = "http://localhost:"; + bindPort + "/solr";
-    if (!SolrCLI.safeCheckCollectionExists(solrUrl, collectionName, null)) {
+    if (!CLIUtils.safeCheckCollectionExists(solrUrl, collectionName, null)) {
       fail(
           "After running Solr cloud example, test collection '"
               + collectionName
diff --git a/solr/core/src/test/org/apache/solr/cloud/SolrCloudExampleTest.java 
b/solr/core/src/test/org/apache/solr/cloud/SolrCloudExampleTest.java
index 1716d09a45f..8e2e728944f 100644
--- a/solr/core/src/test/org/apache/solr/cloud/SolrCloudExampleTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/SolrCloudExampleTest.java
@@ -24,6 +24,7 @@ import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.cli.CommandLine;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.cli.CLIUtils;
 import org.apache.solr.cli.CreateTool;
 import org.apache.solr.cli.DeleteTool;
 import org.apache.solr.cli.HealthcheckTool;
@@ -174,7 +175,7 @@ public class SolrCloudExampleTest extends 
AbstractFullDistribZkTestBase {
     CommandLine cli = SolrCLI.processCommandLineArgs(tool, args);
     assertEquals("Delete action failed!", 0, tool.runTool(cli));
     assertFalse(
-        SolrCLI.safeCheckCollectionExists(
+        CLIUtils.safeCheckCollectionExists(
             solrUrl, testCollectionName, null)); // it should not exist anymore
   }
 }

Reply via email to