diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index d0140d5e2346d..6e96ecd9c5cb5 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -38,6 +38,7 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -894,6 +895,26 @@ static Request createSnapshot(CreateSnapshotRequest createSnapshotRequest) throw return request; } + static Request getSnapshots(GetSnapshotsRequest getSnapshotsRequest) { + EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPartAsIs("_snapshot") + .addPathPart(getSnapshotsRequest.repository()); + String endpoint; + if (getSnapshotsRequest.snapshots().length == 0) { + endpoint = endpointBuilder.addPathPart("_all").build(); + } else { + endpoint = endpointBuilder.addCommaSeparatedPathParts(getSnapshotsRequest.snapshots()).build(); + } + + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + + Params parameters = new Params(request); + parameters.withMasterTimeout(getSnapshotsRequest.masterNodeTimeout()); + parameters.putParam("ignore_unavailable", Boolean.toString(getSnapshotsRequest.ignoreUnavailable())); + parameters.putParam("verbose", Boolean.toString(getSnapshotsRequest.verbose())); + + return request; + } + static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) { String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot") .addPathPart(deleteSnapshotRequest.repository()) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java index 4482fce2edf94..fa147a338de0a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java @@ -32,6 +32,8 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse; import java.io.IOException; @@ -190,6 +192,35 @@ public void createSnapshotAsync(CreateSnapshotRequest createSnapshotRequest, Req CreateSnapshotResponse::fromXContent, listener, emptySet()); } + /** + * Get snapshots. + * See Snapshot and Restore + * API on elastic.co + * + * @param getSnapshotsRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public GetSnapshotsResponse get(GetSnapshotsRequest getSnapshotsRequest, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(getSnapshotsRequest, RequestConverters::getSnapshots, options, + GetSnapshotsResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously get snapshots. + * See Snapshot and Restore + * API on elastic.co + * + * @param getSnapshotsRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void getAsync(GetSnapshotsRequest getSnapshotsRequest, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(getSnapshotsRequest, RequestConverters::getSnapshots, options, + GetSnapshotsResponse::fromXContent, listener, emptySet()); + } + /** * Deletes a snapshot. * See Snapshot and Restore diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 18af52766f159..5887feefa63db 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -39,6 +39,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; import org.elasticsearch.action.admin.indices.alias.Alias; @@ -2011,6 +2012,58 @@ public void testCreateSnapshot() throws IOException { assertToXContentBody(createSnapshotRequest, request.getEntity()); } + public void testGetSnapshots() { + Map expectedParams = new HashMap<>(); + String repository = randomIndicesNames(1, 1)[0]; + String snapshot1 = "snapshot1-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT); + String snapshot2 = "snapshot2-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT); + + String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/%s,%s", repository, snapshot1, snapshot2); + + GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(); + getSnapshotsRequest.repository(repository); + getSnapshotsRequest.snapshots(Arrays.asList(snapshot1, snapshot2).toArray(new String[0])); + setRandomMasterTimeout(getSnapshotsRequest, expectedParams); + + boolean ignoreUnavailable = randomBoolean(); + getSnapshotsRequest.ignoreUnavailable(ignoreUnavailable); + expectedParams.put("ignore_unavailable", Boolean.toString(ignoreUnavailable)); + + boolean verbose = randomBoolean(); + getSnapshotsRequest.verbose(verbose); + expectedParams.put("verbose", Boolean.toString(verbose)); + + Request request = RequestConverters.getSnapshots(getSnapshotsRequest); + assertThat(endpoint, equalTo(request.getEndpoint())); + assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod())); + assertThat(expectedParams, equalTo(request.getParameters())); + assertNull(request.getEntity()); + } + + public void testGetAllSnapshots() { + Map expectedParams = new HashMap<>(); + String repository = randomIndicesNames(1, 1)[0]; + + String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/_all", repository); + + GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(repository); + setRandomMasterTimeout(getSnapshotsRequest, expectedParams); + + boolean ignoreUnavailable = randomBoolean(); + getSnapshotsRequest.ignoreUnavailable(ignoreUnavailable); + expectedParams.put("ignore_unavailable", Boolean.toString(ignoreUnavailable)); + + boolean verbose = randomBoolean(); + getSnapshotsRequest.verbose(verbose); + expectedParams.put("verbose", Boolean.toString(verbose)); + + Request request = RequestConverters.getSnapshots(getSnapshotsRequest); + assertThat(endpoint, equalTo(request.getEndpoint())); + assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod())); + assertThat(expectedParams, equalTo(request.getParameters())); + assertNull(request.getEntity()); + } + public void testDeleteSnapshot() { Map expectedParams = new HashMap<>(); String repository = randomIndicesNames(1, 1)[0]; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java index aacb2f5025ee4..7ec2ee80f04ac 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java @@ -32,12 +32,16 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.RestStatus; import java.io.IOException; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; public class SnapshotIT extends ESRestHighLevelClientTestCase { @@ -135,6 +139,40 @@ public void testCreateSnapshot() throws IOException { assertEquals(waitForCompletion ? RestStatus.OK : RestStatus.ACCEPTED, response.status()); } + public void testGetSnapshots() throws IOException { + String repository = "test_repository"; + String snapshot1 = "test_snapshot1"; + String snapshot2 = "test_snapshot2"; + + PutRepositoryResponse putRepositoryResponse = createTestRepository(repository, FsRepository.TYPE, "{\"location\": \".\"}"); + assertTrue(putRepositoryResponse.isAcknowledged()); + + CreateSnapshotRequest createSnapshotRequest1 = new CreateSnapshotRequest(repository, snapshot1); + createSnapshotRequest1.waitForCompletion(true); + CreateSnapshotResponse putSnapshotResponse1 = createTestSnapshot(createSnapshotRequest1); + CreateSnapshotRequest createSnapshotRequest2 = new CreateSnapshotRequest(repository, snapshot2); + createSnapshotRequest2.waitForCompletion(true); + CreateSnapshotResponse putSnapshotResponse2 = createTestSnapshot(createSnapshotRequest2); + // check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead. + assertEquals(RestStatus.OK, putSnapshotResponse1.status()); + assertEquals(RestStatus.OK, putSnapshotResponse2.status()); + + GetSnapshotsRequest request; + if (randomBoolean()) { + request = new GetSnapshotsRequest(repository); + } else if (randomBoolean()) { + request = new GetSnapshotsRequest(repository, new String[] {"_all"}); + + } else { + request = new GetSnapshotsRequest(repository, new String[] {snapshot1, snapshot2}); + } + GetSnapshotsResponse response = execute(request, highLevelClient().snapshot()::get, highLevelClient().snapshot()::getAsync); + + assertEquals(2, response.getSnapshots().size()); + assertThat(response.getSnapshots().stream().map((s) -> s.snapshotId().getName()).collect(Collectors.toList()), + contains("test_snapshot1", "test_snapshot2")); + } + public void testDeleteSnapshot() throws IOException { String repository = "test_repository"; String snapshot = "test_snapshot"; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java index 9c0e31bdcfb70..e2f976a10c68f 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java @@ -31,6 +31,8 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; +import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -46,6 +48,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.snapshots.SnapshotInfo; import java.io.IOException; import java.util.HashMap; @@ -456,6 +459,76 @@ public void onFailure(Exception exception) { } } + public void testSnapshotGetSnapshots() throws IOException { + RestHighLevelClient client = highLevelClient(); + + createTestRepositories(); + createTestSnapshots(); + + // tag::get-snapshots-request + GetSnapshotsRequest request = new GetSnapshotsRequest(repositoryName); + // end::get-snapshots-request + + // tag::get-snapshots-request-snapshots + String[] snapshots = { snapshotName }; + request.snapshots(snapshots); // <1> + // end::get-snapshots-request-snapshots + + // tag::get-snapshots-request-masterTimeout + request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1> + request.masterNodeTimeout("1m"); // <2> + // end::get-snapshots-request-masterTimeout + + // tag::get-snapshots-request-verbose + request.verbose(true); // <1> + // end::get-snapshots-request-verbose + + // tag::get-snapshots-request-ignore-unavailable + request.ignoreUnavailable(false); // <1> + // end::get-snapshots-request-ignore-unavailable + + // tag::get-snapshots-execute + GetSnapshotsResponse response = client.snapshot().get(request, RequestOptions.DEFAULT); + // end::get-snapshots-execute + + // tag::get-snapshots-response + List snapshotsInfos = response.getSnapshots(); // <1> + // end::get-snapshots-response + assertEquals(1, snapshotsInfos.size()); + } + + public void testSnapshotGetSnapshotsAsync() throws InterruptedException { + RestHighLevelClient client = highLevelClient(); + { + GetSnapshotsRequest request = new GetSnapshotsRequest(); + + // tag::get-snapshots-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(GetSnapshotsResponse deleteSnapshotResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::get-snapshots-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::get-snapshots-execute-async + client.snapshot().getAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::get-snapshots-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testSnapshotDeleteSnapshot() throws IOException { RestHighLevelClient client = highLevelClient(); diff --git a/docs/java-rest/high-level/snapshot/get_snapshots.asciidoc b/docs/java-rest/high-level/snapshot/get_snapshots.asciidoc new file mode 100644 index 0000000000000..388a6904e1391 --- /dev/null +++ b/docs/java-rest/high-level/snapshot/get_snapshots.asciidoc @@ -0,0 +1,103 @@ +[[java-rest-high-snapshot-get-snapshots]] +=== Get Snapshots API + +Use the Get Snapshot API to get snapshots. + +[[java-rest-high-snapshot-get-snapshots-request]] +==== Get Snapshots Request + +A `GetSnapshotsRequest`: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request] +-------------------------------------------------- + +==== Required Arguments +The following arguments are mandatory: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-repositoryName] +-------------------------------------------------- +<1> The name of the repository. + +==== Optional Arguments +The following arguments are optional: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-snapshots] +-------------------------------------------------- +<1> An array of snapshots to get. Otherwise it will return all snapshots for a repository. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-masterTimeout] +-------------------------------------------------- +<1> Timeout to connect to the master node as a `TimeValue`. +<2> Timeout to connect to the master node as a `String`. + + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-verbose] +-------------------------------------------------- +<1> `Boolean` indicating if the response should be verbose. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-ignore-unavailable] +-------------------------------------------------- +<1> `Boolean` indicating if unavailable snapshots should be ignored. Otherwise the request will +fail if any of the snapshots are unavailable. + +[[java-rest-high-snapshot-get-snapshots-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute] +-------------------------------------------------- + +[[java-rest-high-snapshot-get-snapshots-async]] +==== Asynchronous Execution + +The asynchronous execution of a get snapshots request requires both the +`GetSnapshotsRequest` instance and an `ActionListener` instance to be +passed as arguments to the asynchronous method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute-async] +-------------------------------------------------- +<1> The `GetSnapshotsRequest` to execute and the `ActionListener` to use when +the execution completes. + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back with the `onResponse` method +if the execution is successful or the `onFailure` method if the execution +failed. + +A typical listener for `GetSnapshotsResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute-listener +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument. +<2> Called in case of a failure. The raised exception is provided as an +argument. + +[[java-rest-high-snapshot-get-snapshots-response]] +==== Get Snapshots Response + +Use the `GetSnapshotsResponse` to retrieve information about the evaluated +request: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-response] +-------------------------------------------------- +<1> Indicates the node has started the request. \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index de5cd1ebd3dc1..1a126d82cbbae 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -143,6 +143,7 @@ The Java High Level REST Client supports the following Snapshot APIs: * <> * <> * <> +* <> * <> include::snapshot/get_repository.asciidoc[] @@ -150,6 +151,7 @@ include::snapshot/create_repository.asciidoc[] include::snapshot/delete_repository.asciidoc[] include::snapshot/verify_repository.asciidoc[] include::snapshot/create_snapshot.asciidoc[] +include::snapshot/get_snapshots.asciidoc[] include::snapshot/delete_snapshot.asciidoc[] == Tasks APIs diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java index 0d1e5eda7f2d2..6f757cb60ca86 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java @@ -20,23 +20,37 @@ package org.elasticsearch.action.admin.cluster.snapshots.get; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.snapshots.SnapshotInfo; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Get snapshots response */ public class GetSnapshotsResponse extends ActionResponse implements ToXContentObject { + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser GET_SNAPSHOT_PARSER = + new ConstructingObjectParser<>(GetSnapshotsResponse.class.getName(), true, + (args) -> new GetSnapshotsResponse((List) args[0])); + + static { + GET_SNAPSHOT_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), + (p, c) -> SnapshotInfo.SNAPSHOT_INFO_PARSER.apply(p, c).build(), new ParseField("snapshots")); + } + private List snapshots = Collections.emptyList(); GetSnapshotsResponse() { @@ -87,4 +101,20 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params par return builder; } + public static GetSnapshotsResponse fromXContent(XContentParser parser) throws IOException { + return GET_SNAPSHOT_PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetSnapshotsResponse that = (GetSnapshotsResponse) o; + return Objects.equals(snapshots, that.snapshots); + } + + @Override + public int hashCode() { + return Objects.hash(snapshots); + } } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java index ddd7385056d55..a1f56a1e47376 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java @@ -31,15 +31,12 @@ import org.elasticsearch.common.joda.Joda; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ObjectParser; -import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.rest.RestStatus; import java.io.IOException; -import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -84,7 +81,7 @@ public final class SnapshotInfo implements Comparable, ToXContent, private static final Comparator COMPARATOR = Comparator.comparing(SnapshotInfo::startTime).thenComparing(SnapshotInfo::snapshotId); - private static final class SnapshotInfoBuilder { + public static final class SnapshotInfoBuilder { private String snapshotName = null; private String snapshotUUID = null; private String state = null; @@ -137,23 +134,8 @@ private void setVersion(int version) { this.version = version; } - private void setShardFailures(XContentParser parser) { - if (shardFailures == null) { - shardFailures = new ArrayList<>(); - } - - try { - if (parser.currentToken() == Token.START_ARRAY) { - parser.nextToken(); - } - - while (parser.currentToken() != Token.END_ARRAY) { - shardFailures.add(SnapshotShardFailure.fromXContent(parser)); - parser.nextToken(); - } - } catch (IOException exception) { - throw new UncheckedIOException(exception); - } + private void setShardFailures(List shardFailures) { + this.shardFailures = shardFailures; } private void ignoreVersion(String version) { @@ -172,7 +154,7 @@ private void ignoreDurationInMillis(long durationInMillis) { // ignore extra field } - private SnapshotInfo build() { + public SnapshotInfo build() { SnapshotId snapshotId = new SnapshotId(snapshotName, snapshotUUID); if (indices == null) { @@ -219,11 +201,11 @@ private void ignoreFailedShards(int failedShards) { } } - private static final ObjectParser SNAPSHOT_INFO_PARSER = - new ObjectParser<>(SnapshotInfoBuilder.class.getName(), SnapshotInfoBuilder::new); + public static final ObjectParser SNAPSHOT_INFO_PARSER = + new ObjectParser<>(SnapshotInfoBuilder.class.getName(), true, SnapshotInfoBuilder::new); private static final ObjectParser SHARD_STATS_PARSER = - new ObjectParser<>(ShardStatsBuilder.class.getName(), ShardStatsBuilder::new); + new ObjectParser<>(ShardStatsBuilder.class.getName(), true, ShardStatsBuilder::new); static { SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::setSnapshotName, new ParseField(SNAPSHOT)); @@ -236,8 +218,8 @@ private void ignoreFailedShards(int failedShards) { SNAPSHOT_INFO_PARSER.declareObject(SnapshotInfoBuilder::setShardStatsBuilder, SHARD_STATS_PARSER, new ParseField(SHARDS)); SNAPSHOT_INFO_PARSER.declareBoolean(SnapshotInfoBuilder::setIncludeGlobalState, new ParseField(INCLUDE_GLOBAL_STATE)); SNAPSHOT_INFO_PARSER.declareInt(SnapshotInfoBuilder::setVersion, new ParseField(VERSION_ID)); - SNAPSHOT_INFO_PARSER.declareField( - SnapshotInfoBuilder::setShardFailures, parser -> parser, new ParseField(FAILURES), ValueType.OBJECT_ARRAY_OR_STRING); + SNAPSHOT_INFO_PARSER.declareObjectArray(SnapshotInfoBuilder::setShardFailures, SnapshotShardFailure.SNAPSHOT_SHARD_FAILURE_PARSER, + new ParseField(FAILURES)); SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreVersion, new ParseField(VERSION)); SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreStartTime, new ParseField(START_TIME)); SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreEndTime, new ParseField(END_TIME)); @@ -521,7 +503,7 @@ public RestStatus status() { public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException { // write snapshot info to repository snapshot blob format if (CONTEXT_MODE_SNAPSHOT.equals(params.param(CONTEXT_MODE_PARAM))) { - return toXContentSnapshot(builder, params); + return toXContentInternal(builder, params); } final boolean verbose = params.paramAsBoolean("verbose", GetSnapshotsRequest.DEFAULT_VERBOSE_MODE); @@ -576,7 +558,7 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa return builder; } - private XContentBuilder toXContentSnapshot(final XContentBuilder builder, final ToXContent.Params params) throws IOException { + private XContentBuilder toXContentInternal(final XContentBuilder builder, final ToXContent.Params params) throws IOException { builder.startObject(SNAPSHOT); builder.field(NAME, snapshotId.getName()); builder.field(UUID, snapshotId.getUUID()); @@ -609,22 +591,12 @@ private XContentBuilder toXContentSnapshot(final XContentBuilder builder, final return builder; } + /** + * This method creates a SnapshotInfo from external x-content. It does not + * handle x-content written with the internal version. + */ public static SnapshotInfo fromXContent(final XContentParser parser) throws IOException { - parser.nextToken(); // // move to '{' - - if (parser.currentToken() != Token.START_OBJECT) { - throw new IllegalArgumentException("unexpected token [" + parser.currentToken() + "], expected ['{']"); - } - - SnapshotInfo snapshotInfo = SNAPSHOT_INFO_PARSER.apply(parser, null).build(); - - if (parser.currentToken() != Token.END_OBJECT) { - throw new IllegalArgumentException("unexpected token [" + parser.currentToken() + "], expected ['}']"); - } - - parser.nextToken(); // move past '}' - - return snapshotInfo; + return SNAPSHOT_INFO_PARSER.parse(parser, null).build(); } /** diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java index 30057a6220dcb..0df3f0b6ad580 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java @@ -23,8 +23,10 @@ import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -60,11 +62,23 @@ private SnapshotShardFailure() { * @param reason failure reason */ public SnapshotShardFailure(@Nullable String nodeId, ShardId shardId, String reason) { + this(nodeId, shardId, reason, RestStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Constructs new snapshot shard failure object + * + * @param nodeId node where failure occurred + * @param shardId shard id + * @param reason failure reason + * @param status rest status + */ + private SnapshotShardFailure(@Nullable String nodeId, ShardId shardId, String reason, RestStatus status) { + assert reason != null; this.nodeId = nodeId; this.shardId = shardId; this.reason = reason; - assert reason != null; - status = RestStatus.INTERNAL_SERVER_ERROR; + this.status = status; } /** @@ -100,7 +114,7 @@ public String reason() { /** * Returns REST status corresponding to this failure * - * @return REST status + * @return REST STATUS */ @Override public RestStatus status() { @@ -173,63 +187,65 @@ public static void toXContent(SnapshotShardFailure snapshotShardFailure, XConten builder.endObject(); } - /** - * Deserializes snapshot failure information from JSON - * - * @param parser JSON parser - * @return snapshot failure information - */ - public static SnapshotShardFailure fromXContent(XContentParser parser) throws IOException { - SnapshotShardFailure snapshotShardFailure = new SnapshotShardFailure(); - - XContentParser.Token token = parser.currentToken(); - String index = null; - String index_uuid = IndexMetaData.INDEX_UUID_NA_VALUE; - int shardId = -1; - if (token == XContentParser.Token.START_OBJECT) { - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - String currentFieldName = parser.currentName(); - token = parser.nextToken(); - if (token.isValue()) { - if ("index".equals(currentFieldName)) { - index = parser.text(); - } else if ("index_uuid".equals(currentFieldName)) { - index_uuid = parser.text(); - } else if ("node_id".equals(currentFieldName)) { - snapshotShardFailure.nodeId = parser.text(); - } else if ("reason".equals(currentFieldName)) { - // Workaround for https://github.com/elastic/elasticsearch/issues/25878 - // Some old snapshot might still have null in shard failure reasons - snapshotShardFailure.reason = parser.textOrNull(); - } else if ("shard_id".equals(currentFieldName)) { - shardId = parser.intValue(); - } else if ("status".equals(currentFieldName)) { - snapshotShardFailure.status = RestStatus.valueOf(parser.text()); - } else { - throw new ElasticsearchParseException("unknown parameter [{}]", currentFieldName); - } - } - } else { - throw new ElasticsearchParseException("unexpected token [{}]", token); - } - } - } else { - throw new ElasticsearchParseException("unexpected token [{}]", token); - } + static final ConstructingObjectParser SNAPSHOT_SHARD_FAILURE_PARSER = + new ConstructingObjectParser<>("shard_failure", true, SnapshotShardFailure::constructSnapshotShardFailure); + + static { + SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("index")); + SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("index_uuid")); + SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("node_id")); + // Workaround for https://github.com/elastic/elasticsearch/issues/25878 + // Some old snapshot might still have null in shard failure reasons + SNAPSHOT_SHARD_FAILURE_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("reason")); + SNAPSHOT_SHARD_FAILURE_PARSER.declareInt(ConstructingObjectParser.constructorArg(), new ParseField("shard_id")); + SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("status")); + } + + private static SnapshotShardFailure constructSnapshotShardFailure(Object[] args) { + String index = (String) args[0]; + String indexUuid = (String) args[1]; + String nodeId = (String) args[2]; + String reason = (String) args[3]; + Integer intShardId = (Integer) args[4]; + String status = (String) args[5]; + if (index == null) { throw new ElasticsearchParseException("index name was not set"); } - if (shardId == -1) { + if (intShardId == null) { throw new ElasticsearchParseException("index shard was not set"); } - snapshotShardFailure.shardId = new ShardId(index, index_uuid, shardId); + + ShardId shardId = new ShardId(index, indexUuid != null ? indexUuid : IndexMetaData.INDEX_UUID_NA_VALUE, intShardId); + // Workaround for https://github.com/elastic/elasticsearch/issues/25878 // Some old snapshot might still have null in shard failure reasons - if (snapshotShardFailure.reason == null) { - snapshotShardFailure.reason = ""; + String nonNullReason; + if (reason != null) { + nonNullReason = reason; + } else { + nonNullReason = ""; + } + + + RestStatus restStatus; + if (status != null) { + restStatus = RestStatus.valueOf(status); + } else { + restStatus = RestStatus.INTERNAL_SERVER_ERROR; } - return snapshotShardFailure; + + return new SnapshotShardFailure(nodeId, shardId, nonNullReason, restStatus); + } + + /** + * Deserializes snapshot failure information from JSON + * + * @param parser JSON parser + * @return snapshot failure information + */ + public static SnapshotShardFailure fromXContent(XContentParser parser) throws IOException { + return SNAPSHOT_SHARD_FAILURE_PARSER.parse(parser, null); } @Override diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java new file mode 100644 index 0000000000000..c5bd7d9f38ac1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.action.admin.cluster.snapshots.get; + +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; +import org.elasticsearch.snapshots.SnapshotShardFailure; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class GetSnapshotsResponseTests extends AbstractStreamableXContentTestCase { + + @Override + protected GetSnapshotsResponse doParseInstance(XContentParser parser) throws IOException { + return GetSnapshotsResponse.fromXContent(parser); + } + + @Override + protected GetSnapshotsResponse createBlankInstance() { + return new GetSnapshotsResponse(); + } + + @Override + protected GetSnapshotsResponse createTestInstance() { + ArrayList snapshots = new ArrayList<>(); + for (int i = 0; i < randomIntBetween(5, 10); ++i) { + SnapshotId snapshotId = new SnapshotId("snapshot " + i, UUIDs.base64UUID()); + String reason = randomBoolean() ? null : "reason"; + ShardId shardId = new ShardId("index", UUIDs.base64UUID(), 2); + List shardFailures = Collections.singletonList(new SnapshotShardFailure("node-id", shardId, "reason")); + snapshots.add(new SnapshotInfo(snapshotId, Arrays.asList("indice1", "indice2"), System.currentTimeMillis(), reason, + System.currentTimeMillis(), randomIntBetween(2, 3), shardFailures, randomBoolean())); + + } + return new GetSnapshotsResponse(snapshots); + } +}