diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ListClusterNodesApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListClusterNodesApi.java new file mode 100644 index 000000000000..9be2fb477bfa --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ListClusterNodesApi.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.client.api.endpoint; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import org.apache.solr.client.api.model.ListClusterNodesResponse; + +/** V2 API definition for listing the live nodes in the SolrCloud cluster. */ +@Path("/cluster/nodes") +public interface ListClusterNodesApi { + + @GET + @Operation( + summary = "List the live nodes in this Solr cluster.", + tags = {"cluster"}) + ListClusterNodesResponse listClusterNodes(); +} diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java new file mode 100644 index 000000000000..05192655e898 --- /dev/null +++ b/solr/api/src/java/org/apache/solr/client/api/model/ListClusterNodesResponse.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.client.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Set; + +/** Response for the v2 "list cluster nodes" API */ +public class ListClusterNodesResponse extends SolrJerseyResponse { + + @Schema(description = "The live nodes in the cluster.") + @JsonProperty("nodes") + public Set nodes; +} diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java index 36baeb8357f8..d34e7f0169ad 100644 --- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java +++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java @@ -27,7 +27,6 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -41,7 +40,7 @@ import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.CoresApi; import org.apache.solr.client.solrj.request.SystemInfoRequest; import org.apache.solr.client.solrj.response.SystemInfoResponse; @@ -49,7 +48,6 @@ import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkStateReader; 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 { @@ -312,10 +310,11 @@ public static boolean safeCheckCollectionExists( String solrUrl, String collection, String credentials) { boolean exists = false; try (var solrClient = getSolrClient(solrUrl, credentials)) { - NamedList existsCheckResult = solrClient.request(new CollectionAdminRequest.List()); - @SuppressWarnings("unchecked") - List collections = (List) existsCheckResult.get("collections"); - exists = collections != null && collections.contains(collection); + var response = new CollectionsApi.ListCollections().process(solrClient); + exists = + response != null + && response.collections != null + && response.collections.contains(collection); } catch (Exception exc) { // just ignore it since we're only interested in a positive result here } 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 0226471cd13c..4505b619584c 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -32,18 +32,14 @@ import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.client.solrj.request.CoreAdminRequest; +import org.apache.solr.client.solrj.request.CollectionsApi; +import org.apache.solr.client.solrj.request.CoresApi; import org.apache.solr.client.solrj.request.SystemInfoRequest; -import org.apache.solr.client.solrj.response.CoreAdminResponse; +import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.client.solrj.response.SystemInfoResponse; -import org.apache.solr.client.solrj.response.json.JsonMapResponseParser; import org.apache.solr.cloud.ZkConfigSetService; import org.apache.solr.common.cloud.ZkStateReader; -import org.apache.solr.common.util.NamedList; import org.apache.solr.core.ConfigSetService; -import org.noggit.CharArr; -import org.noggit.JSONWriter; /** Supports create command in the bin/solr script. */ public class CreateTool extends ToolBase { @@ -184,14 +180,13 @@ protected void createCore(CommandLine cli, SolrClient solrClient) throws Excepti + coreInstanceDir.toAbsolutePath()); } - echoIfVerbose("\nCreating new core '" + coreName + "' using CoreAdminRequest"); + echoIfVerbose("\nCreating new core '" + coreName + "' using V2 Cores API"); try { - CoreAdminResponse res = CoreAdminRequest.createCore(coreName, coreName, solrClient); - if (isVerbose()) { - echo(res.jsonStr()); - echo("\n"); - } + var req = new CoresApi.CreateCore(); + req.setName(coreName); + req.setInstanceDir(coreName); + req.process(solrClient); echo(String.format(Locale.ROOT, "\nCreated new core '%s'", coreName)); } catch (Exception e) { @@ -280,31 +275,31 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli throw new IllegalStateException( "\nCollection '" + collectionName - + "' already exists!\nChecked collection existence using CollectionAdminRequest"); + + "' already exists!\nChecked collection existence using V2 Collections API"); } // doesn't seem to exist ... try to create - echoIfVerbose( - "\nCreating new collection '" + collectionName + "' using CollectionAdminRequest"); + echoIfVerbose("\nCreating new collection '" + collectionName + "' using V2 Collections API"); - NamedList response; try { - var req = - CollectionAdminRequest.createCollection( - collectionName, confName, numShards, replicationFactor); - req.setResponseParser(new JsonMapResponseParser()); - response = cloudSolrClient.request(req); + var req = new CollectionsApi.CreateCollection(); + req.setName(collectionName); + req.setConfig(confName); + req.setNumShards(numShards); + req.setReplicationFactor(replicationFactor); + var response = req.process(cloudSolrClient); + if (isVerbose() && response != null) { + echo( + JacksonContentWriter.DEFAULT_MAPPER + .writerWithDefaultPrettyPrinter() + .writeValueAsString(response)); + echo("\n"); + } } catch (SolrServerException sse) { throw new Exception( "Failed to create collection '" + collectionName + "' due to: " + sse.getMessage()); } - if (isVerbose()) { - // pretty-print the response to stdout - CharArr arr = new CharArr(); - new JSONWriter(arr, 2).write(response.asMap(10)); - echo(arr.toString()); - } String endMessage = 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 3675098f8eed..1e0233c60ab5 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -29,13 +29,10 @@ import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; -import org.apache.solr.client.solrj.request.CoreAdminRequest; -import org.apache.solr.client.solrj.response.json.JsonMapResponseParser; +import org.apache.solr.client.solrj.request.CollectionsApi; +import org.apache.solr.client.solrj.request.CoresApi; +import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.common.cloud.ZkStateReader; -import org.apache.solr.common.util.NamedList; -import org.noggit.CharArr; -import org.noggit.JSONWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,13 +173,18 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli } } - echoIfVerbose("\nDeleting collection '" + collectionName + "' using CollectionAdminRequest"); + echoIfVerbose("\nDeleting collection '" + collectionName + "' using V2 Collections API"); - NamedList response; try { - var req = CollectionAdminRequest.deleteCollection(collectionName); - req.setResponseParser(new JsonMapResponseParser()); - response = cloudSolrClient.request(req); + var req = new CollectionsApi.DeleteCollection(collectionName); + var response = req.process(cloudSolrClient); + if (isVerbose() && response != null) { + echo( + JacksonContentWriter.DEFAULT_MAPPER + .writerWithDefaultPrettyPrinter() + .writeValueAsString(response)); + echo("\n"); + } } catch (SolrServerException sse) { throw new Exception( "Failed to delete collection '" + collectionName + "' due to: " + sse.getMessage()); @@ -202,38 +204,29 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli } } - if (isVerbose() && response != null) { - // pretty-print the response to stdout - CharArr arr = new CharArr(); - new JSONWriter(arr, 2).write(response.asMap(10)); - echo(arr.toString()); - echo("\n"); - } - echo(String.format(Locale.ROOT, "\nDeleted collection '%s'", collectionName)); } protected void deleteCore(CommandLine cli, SolrClient solrClient) throws Exception { String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION); - echo("\nDeleting core '" + coreName + "' using CoreAdminRequest\n"); + echo("\nDeleting core '" + coreName + "' using V2 Cores API\n"); - NamedList response; try { - CoreAdminRequest.Unload unloadRequest = new CoreAdminRequest.Unload(true); - unloadRequest.setDeleteIndex(true); - unloadRequest.setDeleteDataDir(true); - unloadRequest.setDeleteInstanceDir(true); - unloadRequest.setCoreName(coreName); - unloadRequest.setResponseParser(new JsonMapResponseParser()); - response = solrClient.request(unloadRequest); + var req = new CoresApi.UnloadCore(coreName); + req.setDeleteIndex(true); + req.setDeleteDataDir(true); + req.setDeleteInstanceDir(true); + var response = req.process(solrClient); + if (isVerbose() && response != null) { + echo( + JacksonContentWriter.DEFAULT_MAPPER + .writerWithDefaultPrettyPrinter() + .writeValueAsString(response)); + echo("\n"); + } } catch (SolrServerException sse) { throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage()); } - - if (response != null) { - echoIfVerbose((String) response.get("response")); - echoIfVerbose("\n"); - } } } 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 9fee5940a177..367bfefd66ee 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -19,7 +19,6 @@ import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -31,10 +30,10 @@ import org.apache.commons.cli.Options; import org.apache.solr.cli.SolrProcessManager.SolrProcess; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.ClusterApi; +import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.SystemInfoRequest; import org.apache.solr.client.solrj.response.SystemInfoResponse; -import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.URLUtil; import org.noggit.CharArr; import org.noggit.JSONWriter; @@ -319,23 +318,24 @@ public static Map reportStatus(SolrClient solrClient) throws Exc } /** - * Calls the CLUSTERSTATUS endpoint in Solr to get basic status information about the SolrCloud - * cluster. + * Calls V2 API endpoints to get basic status information about the SolrCloud cluster. + * + *

Uses GET /cluster/nodes for live node count and GET /collections for collection count. */ - @SuppressWarnings("unchecked") private static Map getCloudStatus(SolrClient solrClient, String zkHost) throws Exception { Map cloudStatus = new LinkedHashMap<>(); cloudStatus.put("ZooKeeper", (zkHost != null) ? zkHost : "?"); - // TODO add booleans to request just what we want; not everything - NamedList json = solrClient.request(new CollectionAdminRequest.ClusterStatus()); - - List liveNodes = (List) json._get(List.of("cluster", "live_nodes"), null); - cloudStatus.put("liveNodes", String.valueOf(liveNodes.size())); + var nodesResponse = new ClusterApi.ListClusterNodes().process(solrClient); + var liveNodes = nodesResponse != null ? nodesResponse.nodes : null; + cloudStatus.put("liveNodes", String.valueOf(liveNodes != null ? liveNodes.size() : 0)); - // TODO get this as a metric from the metrics API instead, or something else. - var collections = (Map) json._get(List.of("cluster", "collections"), null); + var collectionsResponse = new CollectionsApi.ListCollections().process(solrClient); + var collections = + collectionsResponse != null && collectionsResponse.collections != null + ? collectionsResponse.collections + : java.util.Collections.emptyList(); cloudStatus.put("collections", String.valueOf(collections.size())); return cloudStatus; diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java index 946559e50161..ffa26e1cab26 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java @@ -194,6 +194,7 @@ import org.apache.solr.handler.admin.api.ForceLeader; import org.apache.solr.handler.admin.api.InstallShardData; import org.apache.solr.handler.admin.api.ListAliases; +import org.apache.solr.handler.admin.api.ListClusterNodes; import org.apache.solr.handler.admin.api.ListCollectionBackups; import org.apache.solr.handler.admin.api.ListCollectionSnapshots; import org.apache.solr.handler.admin.api.ListCollections; @@ -1370,6 +1371,7 @@ public Collection> getJerseyResources() { DeleteNode.class, ListAliases.class, AliasProperty.class, + ListClusterNodes.class, ListCollectionSnapshots.class, CreateCollectionSnapshot.class, DeleteCollectionSnapshot.class, diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java new file mode 100644 index 000000000000..2157109cfcae --- /dev/null +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListClusterNodes.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.handler.admin.api; + +import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM; + +import jakarta.inject.Inject; +import org.apache.solr.client.api.endpoint.ListClusterNodesApi; +import org.apache.solr.client.api.model.ListClusterNodesResponse; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.jersey.PermissionName; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; + +/** + * V2 API for listing the live nodes in the SolrCloud cluster. + * + *

This API (GET /v2/cluster/nodes) is equivalent to the v1 CLUSTERSTATUS action filtered to + * live_nodes. + */ +public class ListClusterNodes extends AdminAPIBase implements ListClusterNodesApi { + + @Inject + public ListClusterNodes( + CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) { + super(coreContainer, req, rsp); + } + + @Override + @PermissionName(COLL_READ_PERM) + public ListClusterNodesResponse listClusterNodes() { + final ListClusterNodesResponse response = + instantiateJerseyResponse(ListClusterNodesResponse.class); + validateZooKeeperAwareCoreContainer(coreContainer); + response.nodes = coreContainer.getZkController().getClusterState().getLiveNodes(); + return response; + } +}