You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2023/08/08 12:17:58 UTC

[solr] branch main updated: SOLR-16800: Move towards using the root of Solr as the -solrUrl in the CLI. (#1808)

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

epugh 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 44668dce33f SOLR-16800: Move towards using the root of Solr as the -solrUrl in the CLI. (#1808)
44668dce33f is described below

commit 44668dce33fbde921cc961079d8db29e83f0f9ca
Author: Eric Pugh <ep...@opensourceconnections.com>
AuthorDate: Tue Aug 8 08:17:51 2023 -0400

    SOLR-16800: Move towards using the root of Solr as the -solrUrl in the CLI. (#1808)
    
    Now that hostContext can only ever be /solr, and the maturing of the Solr V2 API's that are nested under the /api path, we want Solr commands that take in a -solrUrl to point to the root of your Solr, not a nested path.
    
    This handles this, as well as provides feedback to users who still use the older -solrUrl http://localhost:8983/solr form that they no longer need to do this.
---
 solr/CHANGES.txt                                   |  2 +
 .../src/java/org/apache/solr/cli/AssertTool.java   | 17 ++++--
 .../src/java/org/apache/solr/cli/AuthTool.java     |  8 +--
 .../src/java/org/apache/solr/cli/ConfigTool.java   |  5 +-
 .../src/java/org/apache/solr/cli/CreateTool.java   |  2 +-
 .../src/java/org/apache/solr/cli/DeleteTool.java   |  2 +-
 .../src/java/org/apache/solr/cli/ExportTool.java   | 69 +++++++++++-----------
 .../src/java/org/apache/solr/cli/PackageTool.java  | 23 ++------
 .../java/org/apache/solr/cli/RunExampleTool.java   |  4 +-
 .../core/src/java/org/apache/solr/cli/SolrCLI.java | 54 +++++++++++++----
 .../src/java/org/apache/solr/cli/StatusTool.java   | 29 ++++-----
 .../apache/solr/packagemanager/PackageManager.java | 10 ++--
 .../org/apache/solr/cli/HealthcheckToolTest.java   | 14 +++++
 .../{SolrCliUptimeTest.java => SolrCLITest.java}   | 10 +++-
 solr/packaging/test/test_assert.bats               | 22 +++----
 solr/packaging/test/test_config.bats               |  2 +-
 solr/packaging/test/test_create_collection.bats    | 13 +++-
 solr/packaging/test/test_delete_collection.bats    |  8 +++
 solr/packaging/test/test_help.bats                 |  2 +
 solr/packaging/test/test_status.bats               |  1 +
 .../pages/solr-control-script-reference.adoc       | 42 ++++++++++++-
 21 files changed, 219 insertions(+), 120 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 6266da0ace7..312c227110e 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -15,6 +15,8 @@ Improvements
 
 * SOLR-16850: Notify a user if they run healthcheck against standalone Solr that it is a SolrCloud only command. (Eric Pugh)
 
+* SOLR-16800: Solr CLI commands that use -solrUrl now work with a bare url like http://localhost:8983, you no longer need to add the /solr at the end. (Eric Pugh)
+
 Optimizations
 ---------------------
 (No changes)
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 014b19d123a..9ece33ccb8d 100644
--- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java
@@ -29,7 +29,6 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
 import org.apache.solr.client.solrj.request.HealthCheckRequest;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.NamedList;
@@ -198,10 +197,10 @@ public class AssertTool extends ToolBase {
       ret += assertSolrNotRunning(cli.getOptionValue("S"));
     }
     if (cli.hasOption("c")) {
-      ret += assertSolrRunningInCloudMode(cli.getOptionValue("c"));
+      ret += assertSolrRunningInCloudMode(SolrCLI.normalizeSolrUrl(cli.getOptionValue("c")));
     }
     if (cli.hasOption("C")) {
-      ret += assertSolrNotRunningInCloudMode(cli.getOptionValue("C"));
+      ret += assertSolrNotRunningInCloudMode(SolrCLI.normalizeSolrUrl(cli.getOptionValue("C")));
     }
     return ret;
   }
@@ -348,11 +347,11 @@ public class AssertTool extends ToolBase {
     }
   }
 
-  private static int exitOrException(String msg) throws SolrCLI.AssertionFailureException {
+  private static int exitOrException(String msg) throws AssertionFailureException {
     if (useExitCode) {
       return 1;
     } else {
-      throw new SolrCLI.AssertionFailureException(message != null ? message : msg);
+      throw new AssertionFailureException(message != null ? message : msg);
     }
   }
 
@@ -370,8 +369,14 @@ public class AssertTool extends ToolBase {
   }
 
   private static boolean runningSolrIsCloud(String url) throws Exception {
-    try (final SolrClient client = new Http2SolrClient.Builder(url).build()) {
+    try (final SolrClient client = SolrCLI.getSolrClient(url)) {
       return SolrCLI.isCloudMode(client);
     }
   }
+
+  public static class AssertionFailureException extends Exception {
+    public AssertionFailureException(String message) {
+      super(message);
+    }
+  }
 }
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 8d43af55fdb..a502fa77cef 100644
--- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
@@ -111,12 +111,8 @@ public class AuthTool extends ToolBase {
             .desc(
                 "This is where any authentication related configuration files, if any, would be placed.")
             .build(),
-        Option.builder("solrUrl").argName("solrUrl").hasArg().desc("Solr URL.").build(),
-        Option.builder("zkHost")
-            .argName("zkHost")
-            .hasArg()
-            .desc("ZooKeeper host to connect to.")
-            .build(),
+        SolrCLI.OPTION_SOLRURL,
+        SolrCLI.OPTION_ZKHOST,
         SolrCLI.OPTION_VERBOSE);
   }
 
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 e7a4012e263..5221a28325f 100644
--- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java
@@ -24,7 +24,6 @@ import java.util.Map;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
 import org.apache.solr.common.util.NamedList;
 import org.noggit.CharArr;
 import org.noggit.JSONWriter;
@@ -85,7 +84,7 @@ public class ConfigTool extends ToolBase {
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    String solrUrl = SolrCLI.resolveSolrUrl(cli);
+    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
     String action = cli.getOptionValue("action", "set-property");
     String collection = cli.getOptionValue("name");
     String property = cli.getOptionValue("property");
@@ -109,7 +108,7 @@ public class ConfigTool extends ToolBase {
     echo("\nPOSTing request to Config API: " + solrUrl + updatePath);
     echo(jsonBody);
 
-    try (SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build()) {
+    try (SolrClient solrClient = SolrCLI.getSolrClient(solrUrl)) {
       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 e30a30f40bd..9d2f6315469 100644
--- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java
@@ -117,7 +117,7 @@ public class CreateTool extends ToolBase {
   @Override
   public void runImpl(CommandLine cli) throws Exception {
     SolrCLI.raiseLogLevelUnlessVerbose(cli);
-    String solrUrl = cli.getOptionValue("solrUrl", SolrCLI.DEFAULT_SOLR_URL);
+    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
 
     try (var solrClient = SolrCLI.getSolrClient(solrUrl)) {
       if (SolrCLI.isCloudMode(solrClient)) {
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 8d80e029c1c..5e556f02017 100644
--- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java
@@ -88,7 +88,7 @@ public class DeleteTool extends ToolBase {
   @Override
   public void runImpl(CommandLine cli) throws Exception {
     SolrCLI.raiseLogLevelUnlessVerbose(cli);
-    String solrUrl = cli.getOptionValue("solrUrl", SolrCLI.DEFAULT_SOLR_URL);
+    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
 
     try (var solrClient = SolrCLI.getSolrClient(solrUrl)) {
       if (SolrCLI.isCloudMode(solrClient)) {
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 c14550fbcd5..9ea5435e071 100644
--- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java
@@ -94,7 +94,39 @@ public class ExportTool extends ToolBase {
 
   @Override
   public List<Option> getOptions() {
-    return OPTIONS;
+    return List.of(
+        Option.builder("url")
+            .hasArg()
+            .required()
+            .desc("Address of the collection, example http://localhost:8983/solr/gettingstarted.")
+            .build(),
+        Option.builder("out")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Path to output the exported data, and optionally the file name, defaults to 'collection-name'.")
+            .build(),
+        Option.builder("format")
+            .hasArg()
+            .required(false)
+            .desc("Output format for exported docs (json, jsonl or javabin), defaulting to json.")
+            .build(),
+        Option.builder("compress").required(false).desc("Compress the output.").build(),
+        Option.builder("limit")
+            .hasArg()
+            .required(false)
+            .desc("Maximum number of docs to download. Default is 100, use -1 for all docs.")
+            .build(),
+        Option.builder("query")
+            .hasArg()
+            .required(false)
+            .desc("A custom query, default is '*:*'.")
+            .build(),
+        Option.builder("fields")
+            .hasArg()
+            .required(false)
+            .desc("Comma separated list of fields to export. By default all fields are fetched.")
+            .build());
   }
 
   public abstract static class Info {
@@ -230,41 +262,6 @@ public class ExportTool extends ToolBase {
     void end() throws IOException {}
   }
 
-  private static final List<Option> OPTIONS =
-      List.of(
-          Option.builder("url")
-              .hasArg()
-              .required()
-              .desc("Address of the collection, example http://localhost:8983/solr/gettingstarted.")
-              .build(),
-          Option.builder("out")
-              .hasArg()
-              .required(false)
-              .desc(
-                  "Path to output the exported data, and optionally the file name, defaults to 'collection-name'.")
-              .build(),
-          Option.builder("format")
-              .hasArg()
-              .required(false)
-              .desc("Output format for exported docs (json, jsonl or javabin), defaulting to json.")
-              .build(),
-          Option.builder("compress").required(false).desc("Compress the output.").build(),
-          Option.builder("limit")
-              .hasArg()
-              .required(false)
-              .desc("Maximum number of docs to download. Default is 100, use -1 for all docs.")
-              .build(),
-          Option.builder("query")
-              .hasArg()
-              .required(false)
-              .desc("A custom query, default is '*:*'.")
-              .build(),
-          Option.builder("fields")
-              .hasArg()
-              .required(false)
-              .desc("Comma separated list of fields to export. By default all fields are fetched.")
-              .build());
-
   static class JsonWithLinesSink extends DocsSink {
     private final CharArr charArr = new CharArr(1024 * 2);
     JSONWriter jsonWriter = new JSONWriter(charArr, -1);
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 6a8714ddb10..97ed80e7be3 100644
--- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java
@@ -60,8 +60,6 @@ public class PackageTool extends ToolBase {
     return "package";
   }
 
-  public static String solrUrl = null;
-  public static String solrBaseUrl = null;
   public PackageManager packageManager;
   public RepositoryManager repositoryManager;
 
@@ -79,10 +77,7 @@ public class PackageTool extends ToolBase {
         printHelp();
         return;
       }
-
-      solrUrl = cli.getOptionValues("solrUrl")[cli.getOptionValues("solrUrl").length - 1];
-      solrBaseUrl = solrUrl.replaceAll("/solr$", ""); // strip out ending "/solr"
-      log.info("Solr url:{}, solr base url: {}", solrUrl, solrBaseUrl);
+      String solrUrl = SolrCLI.normalizeSolrUrl(cli);
       String zkHost = SolrCLI.getZkHost(cli);
       if (zkHost == null) {
         throw new SolrException(ErrorCode.INVALID_STATE, "Package manager runs only in SolrCloud");
@@ -90,8 +85,8 @@ public class PackageTool extends ToolBase {
 
       log.info("ZK: {}", zkHost);
 
-      try (SolrClient solrClient = new Http2SolrClient.Builder(solrBaseUrl).build()) {
-        packageManager = new PackageManager(solrClient, solrBaseUrl, zkHost);
+      try (SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build()) {
+        packageManager = new PackageManager(solrClient, solrUrl, zkHost);
         try {
           repositoryManager = new RepositoryManager(solrClient, packageManager);
 
@@ -242,7 +237,7 @@ public class PackageTool extends ToolBase {
 
     } catch (Exception ex) {
       // We need to print this since SolrCLI drops the stack trace in favour
-      // of brevity. Package tool should surely print full stacktraces!
+      // of brevity. Package tool should surely print the full stacktrace!
       ex.printStackTrace();
       throw ex;
     }
@@ -314,15 +309,7 @@ public class PackageTool extends ToolBase {
   @Override
   public List<Option> getOptions() {
     return List.of(
-        Option.builder("solrUrl")
-            .argName("URL")
-            .hasArg()
-            .required(true)
-            .desc(
-                "Address of the Solr Web application, defaults to: "
-                    + SolrCLI.DEFAULT_SOLR_URL
-                    + '.')
-            .build(),
+        SolrCLI.OPTION_SOLRURL,
         Option.builder("collections")
             .argName("COLLECTIONS")
             .hasArg()
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 41ddffc0507..80dcdb41c72 100644
--- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
@@ -400,7 +400,7 @@ public class RunExampleTool extends ToolBase {
                 + "        }\n");
 
         File filmsJsonFile = new File(exampleDir, "films/films.json");
-        String updateUrl = String.format(Locale.ROOT, "%s/%s/update/json", solrUrl, collectionName);
+        String updateUrl = String.format(Locale.ROOT, "%s/%s/update", solrUrl, collectionName);
         echo("Indexing films example docs from " + filmsJsonFile.getAbsolutePath());
         String[] args =
             new String[] {
@@ -410,6 +410,8 @@ public class RunExampleTool extends ToolBase {
               updateUrl,
               "-type",
               "application/json",
+              "-filetypes",
+              "json",
               exampleDir.toString()
             };
         PostTool postTool = new PostTool();
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 c0a385a0198..c3ddd46b85b 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -75,7 +75,7 @@ public class SolrCLI implements CLIO {
       TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-  public static final String DEFAULT_SOLR_URL = "http://localhost:8983/solr";
+  public static final String DEFAULT_SOLR_URL = "http://localhost:8983";
   public static final String ZK_HOST = "localhost:9983";
 
   public static final Option OPTION_ZKHOST =
@@ -86,7 +86,8 @@ public class SolrCLI implements CLIO {
           .required(false)
           .desc(
               "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to "
-                  + ZK_HOST)
+                  + ZK_HOST
+                  + '.')
           .longOpt("zkHost")
           .build();
   public static final Option OPTION_SOLRURL =
@@ -96,7 +97,8 @@ public class SolrCLI implements CLIO {
           .required(false)
           .desc(
               "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
-                  + DEFAULT_SOLR_URL)
+                  + DEFAULT_SOLR_URL
+                  + '.')
           .build();
   public static final Option OPTION_VERBOSE =
       Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
@@ -382,6 +384,13 @@ public class SolrCLI implements CLIO {
   }
 
   public static SolrClient getSolrClient(String solrUrl) {
+    // today we require all urls to end in /solr, however in the future we will need to support the
+    // /api url end point instead.
+    // The /solr/ check is because sometimes a full url is passed in, like
+    // http://localhost:8983/solr/films_shard1_replica_n1/.
+    if (!solrUrl.endsWith("/solr") && !solrUrl.contains("/solr/")) {
+      solrUrl = solrUrl + "/solr";
+    }
     return new Http2SolrClient.Builder(solrUrl).withMaxConnectionsPerHost(32).build();
   }
 
@@ -444,11 +453,37 @@ public class SolrCLI implements CLIO {
     print("Pass -help or -h after any COMMAND to see command-specific usage information,");
     print("such as:    ./solr start -help or ./solr stop -h");
   }
+
   /**
-   * Get the base URL of a live Solr instance from either the solrUrl command-line option from
+   * 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 url's 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) {
+    if (solrUrl != null) {
+      if (solrUrl.indexOf("/solr") > -1) { //
+        String newSolrUrl = solrUrl.substring(0, solrUrl.indexOf("/solr"));
+        CLIO.out(
+            "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 solrUrl command-line option or from
    * ZooKeeper.
    */
-  public static String resolveSolrUrl(CommandLine cli) throws Exception {
+  public static String normalizeSolrUrl(CommandLine cli) throws Exception {
     String solrUrl = cli.getOptionValue("solrUrl");
     if (solrUrl == null) {
       String zkHost = cli.getOptionValue("zkHost");
@@ -475,6 +510,7 @@ public class SolrCLI implements CLIO {
         }
       }
     }
+    solrUrl = normalizeSolrUrl(solrUrl);
     return solrUrl;
   }
 
@@ -488,7 +524,6 @@ public class SolrCLI implements CLIO {
       return zkHost;
     }
 
-    // find it using the localPort
     String solrUrl = cli.getOptionValue("solrUrl");
     if (solrUrl == null) {
       solrUrl = DEFAULT_SOLR_URL;
@@ -498,6 +533,7 @@ public class SolrCLI implements CLIO {
                   + DEFAULT_SOLR_URL
                   + ".");
     }
+    solrUrl = normalizeSolrUrl(solrUrl);
 
     try (var solrClient = getSolrClient(solrUrl)) {
       // hit Solr to get system info
@@ -568,10 +604,4 @@ public class SolrCLI implements CLIO {
         solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, SYSTEM_INFO_PATH));
     return "solrcloud".equals(systemInfo.get("mode"));
   }
-
-  public static class AssertionFailureException extends Exception {
-    public AssertionFailureException(String message) {
-      super(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 349804ca7d0..546dc742f3d 100644
--- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
@@ -57,42 +57,43 @@ public class StatusTool extends ToolBase {
     return "status";
   }
 
-  // These options are not exposed to the end user, and are
-  // used directly by the bin/solr status CLI.
+  public static final Option OPTION_MAXWAITSECS =
+      Option.builder("maxWaitSecs")
+          .argName("SECS")
+          .hasArg()
+          .required(false)
+          .desc("Wait up to the specified number of seconds to see Solr running.")
+          .build();
+
   @Override
   public List<Option> getOptions() {
     return List.of(
-        // Unlike most of the other tools, this is an internal, not end user
-        // focused setting.  Therefore, no default value is provided.
+        // The solrUrl option is not exposed to the end user, and is
+        // created by the bin/solr script and passed into this too.
         Option.builder("solrUrl")
             .argName("URL")
             .hasArg()
             .required(false)
             .desc("Property set by calling scripts, not meant for user configuration.")
             .build(),
-        Option.builder("maxWaitSecs")
-            .argName("SECS")
-            .hasArg()
-            .required(false)
-            .desc("Wait up to the specified number of seconds to see Solr running.")
-            .build());
+        OPTION_MAXWAITSECS);
   }
 
   @Override
   public void runImpl(CommandLine cli) throws Exception {
-    // Override the default help behaviour to put out a customized message that omits the internally
-    // focused Options.
+    // Override the default help behaviour to put out a customized message that only list user
+    // settable Options.
     if ((cli.getOptions().length == 0 && cli.getArgs().length == 0)
         || cli.hasOption("h")
         || cli.hasOption("help")) {
       final Options options = new Options();
-      getOptions().forEach(options::addOption);
+      options.addOption(OPTION_MAXWAITSECS);
       new HelpFormatter().printHelp("status", options);
       return;
     }
 
     int maxWaitSecs = Integer.parseInt(cli.getOptionValue("maxWaitSecs", "0"));
-    String solrUrl = cli.getOptionValue("solrUrl");
+    String solrUrl = SolrCLI.normalizeSolrUrl(cli);
     if (maxWaitSecs > 0) {
       int solrPort = (new URL(solrUrl)).getPort();
       echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on port " + solrPort);
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 5554d0dd022..beefd88653d 100644
--- a/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
+++ b/solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java
@@ -69,7 +69,7 @@ import org.slf4j.LoggerFactory;
 /** Handles most of the management of packages that are already installed in Solr. */
 public class PackageManager implements Closeable {
 
-  final String solrBaseUrl;
+  final String solrUrl;
   final SolrClient solrClient;
   final SolrZkClient zkClient;
 
@@ -77,8 +77,8 @@ public class PackageManager implements Closeable {
 
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  public PackageManager(SolrClient solrClient, String solrBaseUrl, String zkHost) {
-    this.solrBaseUrl = solrBaseUrl;
+  public PackageManager(SolrClient solrClient, String solrUrl, String zkHost) {
+    this.solrUrl = solrUrl;
     this.solrClient = solrClient;
     this.zkClient =
         new SolrZkClient.Builder()
@@ -760,7 +760,7 @@ public class PackageManager implements Closeable {
                   plugin.name);
           String path =
               PackageUtils.resolve(cmd.path, pkg.parameterDefaults, overridesMap, systemParams);
-          PackageUtils.printGreen("Executing " + solrBaseUrl + path + " for cluster level plugin");
+          PackageUtils.printGreen("Executing " + solrUrl + path + " for cluster level plugin");
 
           if ("GET".equalsIgnoreCase(cmd.method)) {
             String response =
@@ -811,7 +811,7 @@ public class PackageManager implements Closeable {
                 PackageUtils.resolve(
                     cmd.path, pkg.parameterDefaults, collectionParameterOverrides, systemParams);
             PackageUtils.printGreen(
-                "Executing " + solrBaseUrl + path + " for collection:" + collection);
+                "Executing " + solrUrl + path + " for collection:" + collection);
 
             if ("GET".equalsIgnoreCase(cmd.method)) {
               String response =
diff --git a/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
index fd9a4a0c230..a7d8e7150eb 100644
--- a/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/HealthcheckToolTest.java
@@ -65,6 +65,20 @@ public class HealthcheckToolTest extends SolrCloudTestCase {
     assertEquals(0, runTool(args));
   }
 
+  @Test
+  public void testHealthcheckWithSolrRootUrlParameter() throws Exception {
+
+    Set<String> liveNodes = cluster.getSolrClient().getClusterState().getLiveNodes();
+    String firstLiveNode = liveNodes.iterator().next();
+    String solrUrl =
+        ZkStateReader.from(cluster.getSolrClient()).getBaseUrlForNodeName(firstLiveNode);
+
+    solrUrl = solrUrl.substring(0, solrUrl.indexOf("/solr"));
+
+    String[] args = new String[] {"healthcheck", "-c", "bob", "-solrUrl", solrUrl};
+    assertEquals(0, runTool(args));
+  }
+
   private int runTool(String[] args) throws Exception {
     Tool tool = findTool(args);
     assertTrue(tool instanceof HealthcheckTool);
diff --git a/solr/core/src/test/org/apache/solr/cli/SolrCliUptimeTest.java b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
similarity index 75%
rename from solr/core/src/test/org/apache/solr/cli/SolrCliUptimeTest.java
rename to solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
index 4589ea539ef..03f35246211 100644
--- a/solr/core/src/test/org/apache/solr/cli/SolrCliUptimeTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java
@@ -19,7 +19,15 @@ package org.apache.solr.cli;
 import org.apache.solr.SolrTestCase;
 import org.junit.Test;
 
-public class SolrCliUptimeTest extends SolrTestCase {
+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");
+  }
+
   @Test
   public void testUptime() {
     assertEquals("?", SolrCLI.uptime(0));
diff --git a/solr/packaging/test/test_assert.bats b/solr/packaging/test/test_assert.bats
index 991774add01..66905a39775 100644
--- a/solr/packaging/test/test_assert.bats
+++ b/solr/packaging/test/test_assert.bats
@@ -30,24 +30,26 @@ teardown() {
 
 @test "assert for non cloud mode" {
   run solr start
-  
+
   run solr assert --not-cloud http://localhost:8983/solr
+  assert_output --partial "needn't include Solr's context-root"
   refute_output --partial "ERROR"
-  
-  run solr assert --cloud http://localhost:8983/solr
+
+  run solr assert --cloud http://localhost:8983
   assert_output --partial "ERROR: Solr is not running in cloud mode"
-  
+
   run ! solr assert --cloud http://localhost:8983/solr -e
 }
 
 @test "assert for cloud mode" {
-  run solr start -c 
-  
-  run solr assert --cloud http://localhost:8983/solr
+  run solr start -c
+
+  run solr assert --cloud http://localhost:8983
   refute_output --partial "ERROR"
-  
+
   run solr assert --not-cloud http://localhost:8983/solr
+  assert_output --partial "needn't include Solr's context-root"
   assert_output --partial "ERROR: Solr is not running in standalone mode"
-  
-  run ! solr assert --not-cloud http://localhost:8983/solr -e
+
+  run ! solr assert --not-cloud http://localhost:8983 -e
 }
diff --git a/solr/packaging/test/test_config.bats b/solr/packaging/test/test_config.bats
index b588e0c10db..aff38d25eb9 100644
--- a/solr/packaging/test/test_config.bats
+++ b/solr/packaging/test/test_config.bats
@@ -50,5 +50,5 @@ teardown() {
 
   run solr config -c COLL_NAME -property updateHandler.autoCommit.maxDocs -value 100
   assert_output --partial "Successfully set-property updateHandler.autoCommit.maxDocs to 100"
-  assert_output --partial "assuming solrUrl is http://localhost:8983/solr"
+  assert_output --partial "assuming solrUrl is http://localhost:8983."
 }
diff --git a/solr/packaging/test/test_create_collection.bats b/solr/packaging/test/test_create_collection.bats
index ff776609378..64c90f87087 100644
--- a/solr/packaging/test/test_create_collection.bats
+++ b/solr/packaging/test/test_create_collection.bats
@@ -41,13 +41,20 @@ teardown() {
 @test "create collection" {
   run solr create -c COLL_NAME
   assert_output --partial "Created collection 'COLL_NAME'"
-  assert_output --partial "assuming solrUrl is http://localhost:8983/solr"
+  assert_output --partial "assuming solrUrl is http://localhost:8983"
 }
 
 @test "create collection using solrUrl" {
+  run solr create -c COLL_NAME -solrUrl http://localhost:8983
+  assert_output --partial "Created collection 'COLL_NAME'"
+  refute_output --partial "assuming solrUrl is http://localhost:8983"
+}
+
+@test "create collection using legacy solrUrl" {
   run solr create -c COLL_NAME -solrUrl http://localhost:8983/solr
-  assert_output --partial "Created collection 'COLL_NAME'"  
-  refute_output --partial "assuming solrUrl is http://localhost:8983/solr"
+  assert_output --partial "Created collection 'COLL_NAME'"
+  assert_output --partial "needn't include Solr's context-root"
+  refute_output --partial "assuming solrUrl is http://localhost:8983"
 }
 
 @test "create collection using Zookeeper" {
diff --git a/solr/packaging/test/test_delete_collection.bats b/solr/packaging/test/test_delete_collection.bats
index 6e71d1b52ea..bbef50dce10 100644
--- a/solr/packaging/test/test_delete_collection.bats
+++ b/solr/packaging/test/test_delete_collection.bats
@@ -46,6 +46,14 @@ teardown() {
   refute collection_exists "COLL_NAME"
 }
 
+@test "can delete collections with solrUrl" {
+  solr create -c "COLL_NAME"
+  assert collection_exists "COLL_NAME"
+
+  solr delete -c "COLL_NAME" -solrUrl http://localhost:8983
+  refute collection_exists "COLL_NAME"
+}
+
 @test "collection delete also deletes zk config" {
   solr create -c "COLL_NAME"
   assert config_exists "COLL_NAME"
diff --git a/solr/packaging/test/test_help.bats b/solr/packaging/test/test_help.bats
index a6a8a497b36..3f929eee351 100644
--- a/solr/packaging/test/test_help.bats
+++ b/solr/packaging/test/test_help.bats
@@ -62,6 +62,8 @@ setup() {
   run solr status -help
   assert_output --partial 'usage: status'
   refute_output --partial 'ERROR'
+  # Make sure custom selection of options for status help works.
+  refute_output --partial '-solrUrl'
 }
 
 @test "healthcheck help flag prints help" {
diff --git a/solr/packaging/test/test_status.bats b/solr/packaging/test/test_status.bats
index 64885dc7f52..292de5ca008 100644
--- a/solr/packaging/test/test_status.bats
+++ b/solr/packaging/test/test_status.bats
@@ -43,6 +43,7 @@ teardown() {
 @test "status shell script ignores passed in -solrUrl cli parameter from user" {
   solr start
   run solr status -solrUrl http://localhost:9999/solr
+  assert_output --partial "needn't include Solr's context-root"
   assert_output --partial "Found 1 Solr nodes:"
   assert_output --partial "running on port 8983"
 }
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
index 12e1a4ea0f6..4897a1a2c34 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc
@@ -553,7 +553,7 @@ Name of the collection to run a healthcheck against.
 +
 [%autowidth,frame=none]
 |===
-|Optional |Default: none
+|Optional |Default: `http://localhost:8983`
 |===
 +
 Base Solr URL, which can be used in SolrCloud mode to determine the ZooKeeper connection string if that's not known.
@@ -710,6 +710,25 @@ It is possible to override this warning with the -force parameter.
 +
 *Example*: `bin/solr create -c foo -force`
 
+`-z <zkHost>` or `-zkHost <zkHost>`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `localhost:9983`
+|===
++
+The ZooKeeper connection string, usable in SolrCloud mode.
+Unnecessary if `ZK_HOST` is defined in `solr.in.sh` or `solr.in.cmd`.
+
+`-solrUrl <url>`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `http://localhost:8983`
+|===
++
+Base Solr URL, which can be used in SolrCloud mode to determine the ZooKeeper connection string if that's not known.
+
 ==== Configuration Directories and SolrCloud
 
 Before creating a collection in SolrCloud, the configuration directory used by the collection must be uploaded to ZooKeeper.
@@ -800,6 +819,25 @@ If the configuration directory is being used by another collection, then it will
 +
 Skip safety checks when deleting the configuration directory used by a collection.
 
+`-z <zkHost>` or `-zkHost <zkHost>`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `localhost:9983`
+|===
++
+The ZooKeeper connection string, usable in SolrCloud mode.
+Unnecessary if `ZK_HOST` is defined in `solr.in.sh` or `solr.in.cmd`.
+
+`-solrUrl <url>`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `http://localhost:8983`
+|===
++
+Base Solr URL, which can be used in SolrCloud mode to determine the ZooKeeper connection string if that's not known.
+
 == Authentication
 
 The `bin/solr` script allows enabling or disabling Basic Authentication, allowing you to configure authentication from the command line.
@@ -1048,7 +1086,7 @@ Unnecessary if `ZK_HOST` is defined in `solr.in.sh` or `solr.in.cmd`.
 +
 [%autowidth,frame=none]
 |===
-|Optional |Default: `http://localhost:8983/solr`
+|Optional |Default: `http://localhost:8983`
 |===
 +
 Base Solr URL, which can be used in SolrCloud mode to determine the ZooKeeper connection string if that's not known.