From 73a38895fd8e6c38eea726686eb2fce26ee569d0 Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Tue, 24 Jul 2018 16:17:09 +0200 Subject: [PATCH] Add Restore Snapshot High Level REST API With this commit we add the restore snapshot API to the Java high level REST client. Relates #27205 Relates #32155 --- .../client/RequestConverters.java | 15 ++ .../elasticsearch/client/SnapshotClient.java | 32 ++++ .../client/RequestConvertersTests.java | 26 ++++ .../client/RestHighLevelClientTests.java | 1 - .../org/elasticsearch/client/SnapshotIT.java | 41 +++++ .../SnapshotClientDocumentationIT.java | 106 +++++++++++++ .../snapshot/restore_snapshot.asciidoc | 144 ++++++++++++++++++ .../restore/RestoreSnapshotRequest.java | 81 +++++++++- .../restore/RestoreSnapshotResponse.java | 44 ++++++ .../elasticsearch/snapshots/RestoreInfo.java | 52 +++++-- .../restore/RestoreSnapshotRequestTests.java | 141 +++++++++++++++++ .../restore/RestoreSnapshotResponseTests.java | 56 +++++++ .../test/rest/ESRestTestCase.java | 5 + 13 files changed, 732 insertions(+), 12 deletions(-) create mode 100644 docs/java-rest/high-level/snapshot/restore_snapshot.asciidoc create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestTests.java create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotResponseTests.java 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 a6122b0681e..b486d9fce52 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 @@ -40,6 +40,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsRequest 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.snapshots.restore.RestoreSnapshotRequest; 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; @@ -980,6 +981,20 @@ final class RequestConverters { return request; } + static Request restoreSnapshot(RestoreSnapshotRequest restoreSnapshotRequest) throws IOException { + String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot") + .addPathPart(restoreSnapshotRequest.repository()) + .addPathPart(restoreSnapshotRequest.snapshot()) + .addPathPartAsIs("_restore") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + Params parameters = new Params(request); + parameters.withMasterTimeout(restoreSnapshotRequest.masterNodeTimeout()); + parameters.withWaitForCompletion(restoreSnapshotRequest.waitForCompletion()); + request.setEntity(createEntity(restoreSnapshotRequest, REQUEST_BODY_CONTENT_TYPE)); + 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 ae115839bae..319eb96a9f8 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 @@ -30,6 +30,8 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyReposito 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.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -252,6 +254,36 @@ public final class SnapshotClient { SnapshotsStatusResponse::fromXContent, listener, emptySet()); } + /** + * Restores a snapshot. + * See Snapshot and Restore + * API on elastic.co + * + * @param restoreSnapshotRequest 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 RestoreSnapshotResponse restore(RestoreSnapshotRequest restoreSnapshotRequest, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(restoreSnapshotRequest, RequestConverters::restoreSnapshot, options, + RestoreSnapshotResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously restores a snapshot. + * See Snapshot and Restore + * API on elastic.co + * + * @param restoreSnapshotRequest 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 restoreAsync(RestoreSnapshotRequest restoreSnapshotRequest, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(restoreSnapshotRequest, RequestConverters::restoreSnapshot, options, + RestoreSnapshotResponse::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 c1f47feb33d..0415d363c54 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 @@ -41,6 +41,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequ 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.snapshots.restore.RestoreSnapshotRequest; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest; @@ -2198,6 +2199,31 @@ public class RequestConvertersTests extends ESTestCase { assertThat(request.getEntity(), is(nullValue())); } + public void testRestoreSnapshot() throws IOException { + Map expectedParams = new HashMap<>(); + String repository = randomIndicesNames(1, 1)[0]; + String snapshot = "snapshot-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT); + String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/%s/_restore", repository, snapshot); + + RestoreSnapshotRequest restoreSnapshotRequest = new RestoreSnapshotRequest(repository, snapshot); + setRandomMasterTimeout(restoreSnapshotRequest, expectedParams); + if (randomBoolean()) { + restoreSnapshotRequest.waitForCompletion(true); + expectedParams.put("wait_for_completion", "true"); + } + if (randomBoolean()) { + String timeout = randomTimeValue(); + restoreSnapshotRequest.masterNodeTimeout(timeout); + expectedParams.put("master_timeout", timeout); + } + + Request request = RequestConverters.restoreSnapshot(restoreSnapshotRequest); + assertThat(endpoint, equalTo(request.getEndpoint())); + assertThat(HttpPost.METHOD_NAME, equalTo(request.getMethod())); + assertThat(expectedParams, equalTo(request.getParameters())); + assertToXContentBody(restoreSnapshotRequest, 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/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 48934a9bed8..5cf3b352756 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -665,7 +665,6 @@ public class RestHighLevelClientTests extends ESTestCase { "reindex_rethrottle", "render_search_template", "scripts_painless_execute", - "snapshot.restore", "tasks.get", "termvectors", "update_by_query" 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 5483f055c2c..06aec70a018 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 @@ -28,6 +28,8 @@ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryResponse; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest; import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusRequest; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; import org.elasticsearch.common.settings.Settings; @@ -40,12 +42,15 @@ 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 org.elasticsearch.snapshots.RestoreInfo; import java.io.IOException; +import java.util.Collections; import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; public class SnapshotIT extends ESRestHighLevelClientTestCase { @@ -205,6 +210,42 @@ public class SnapshotIT extends ESRestHighLevelClientTestCase { assertThat(response.getSnapshots().get(0).getIndices().containsKey(testIndex), is(true)); } + public void testRestoreSnapshot() throws IOException { + String testRepository = "test"; + String testSnapshot = "snapshot_1"; + String testIndex = "test_index"; + String restoredIndex = testIndex + "_restored"; + + PutRepositoryResponse putRepositoryResponse = createTestRepository(testRepository, FsRepository.TYPE, "{\"location\": \".\"}"); + assertTrue(putRepositoryResponse.isAcknowledged()); + + createIndex(testIndex, Settings.EMPTY); + assertTrue("index [" + testIndex + "] should have been created", indexExists(testIndex)); + + CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(testRepository, testSnapshot); + createSnapshotRequest.indices(testIndex); + createSnapshotRequest.waitForCompletion(true); + CreateSnapshotResponse createSnapshotResponse = createTestSnapshot(createSnapshotRequest); + assertEquals(RestStatus.OK, createSnapshotResponse.status()); + + deleteIndex(testIndex); + assertFalse("index [" + testIndex + "] should have been deleted", indexExists(testIndex)); + + RestoreSnapshotRequest request = new RestoreSnapshotRequest(testRepository, testSnapshot); + request.waitForCompletion(true); + request.renamePattern(testIndex); + request.renameReplacement(restoredIndex); + + RestoreSnapshotResponse response = execute(request, highLevelClient().snapshot()::restore, + highLevelClient().snapshot()::restoreAsync); + + RestoreInfo restoreInfo = response.getRestoreInfo(); + assertThat(restoreInfo.name(), equalTo(testSnapshot)); + assertThat(restoreInfo.indices(), equalTo(Collections.singletonList(restoredIndex))); + assertThat(restoreInfo.successfulShards(), greaterThan(0)); + assertThat(restoreInfo.failedShards(), equalTo(0)); + } + 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 fff3e7ece70..922fcb984d9 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 @@ -33,6 +33,8 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotReq 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.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest; @@ -53,12 +55,15 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.snapshots.RestoreInfo; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotShardFailure; import org.elasticsearch.snapshots.SnapshotState; import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -263,6 +268,107 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase } } + public void testRestoreSnapshot() throws IOException { + RestHighLevelClient client = highLevelClient(); + + createTestRepositories(); + createTestIndex(); + createTestSnapshots(); + + // tag::restore-snapshot-request + RestoreSnapshotRequest request = new RestoreSnapshotRequest(repositoryName, snapshotName); + // end::restore-snapshot-request + // we need to restore as a different index name + + // tag::restore-snapshot-request-masterTimeout + request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1> + request.masterNodeTimeout("1m"); // <2> + // end::restore-snapshot-request-masterTimeout + + // tag::restore-snapshot-request-waitForCompletion + request.waitForCompletion(true); // <1> + // end::restore-snapshot-request-waitForCompletion + + // tag::restore-snapshot-request-partial + request.partial(false); // <1> + // end::restore-snapshot-request-partial + + // tag::restore-snapshot-request-include-global-state + request.includeGlobalState(false); // <1> + // end::restore-snapshot-request-include-global-state + + // tag::restore-snapshot-request-include-aliases + request.includeAliases(false); // <1> + // end::restore-snapshot-request-include-aliases + + + // tag::restore-snapshot-request-indices + request.indices("test_index"); + // end::restore-snapshot-request-indices + + String restoredIndexName = "restored_index"; + // tag::restore-snapshot-request-rename + request.renamePattern("test_(.+)"); // <1> + request.renameReplacement("restored_$1"); // <2> + // end::restore-snapshot-request-rename + + // tag::restore-snapshot-request-index-settings + request.indexSettings( // <1> + Settings.builder() + .put("index.number_of_replicas", 0) + .build()); + + request.ignoreIndexSettings("index.refresh_interval", "index.search.idle.after"); // <2> + request.indicesOptions(new IndicesOptions( // <3> + EnumSet.of(IndicesOptions.Option.IGNORE_UNAVAILABLE), + EnumSet.of(IndicesOptions.WildcardStates.OPEN))); + // end::restore-snapshot-request-index-settings + + // tag::restore-snapshot-execute + RestoreSnapshotResponse response = client.snapshot().restore(request, RequestOptions.DEFAULT); + // end::restore-snapshot-execute + + // tag::restore-snapshot-response + RestoreInfo restoreInfo = response.getRestoreInfo(); + List indices = restoreInfo.indices(); // <1> + // end::restore-snapshot-response + assertEquals(Collections.singletonList(restoredIndexName), indices); + assertEquals(0, restoreInfo.failedShards()); + assertTrue(restoreInfo.successfulShards() > 0); + } + + public void testRestoreSnapshotAsync() throws InterruptedException { + RestHighLevelClient client = highLevelClient(); + { + RestoreSnapshotRequest request = new RestoreSnapshotRequest(); + + // tag::restore-snapshot-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(RestoreSnapshotResponse restoreSnapshotResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::restore-snapshot-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::restore-snapshot-execute-async + client.snapshot().restoreAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::restore-snapshot-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testSnapshotDeleteRepository() throws IOException { RestHighLevelClient client = highLevelClient(); diff --git a/docs/java-rest/high-level/snapshot/restore_snapshot.asciidoc b/docs/java-rest/high-level/snapshot/restore_snapshot.asciidoc new file mode 100644 index 00000000000..a4b83ca419a --- /dev/null +++ b/docs/java-rest/high-level/snapshot/restore_snapshot.asciidoc @@ -0,0 +1,144 @@ +[[java-rest-high-snapshot-restore-snapshot]] +=== Restore Snapshot API + +The Restore Snapshot API allows to restore a snapshot. + +[[java-rest-high-snapshot-restore-snapshot-request]] +==== Restore Snapshot Request + +A `RestoreSnapshotRequest`: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request] +-------------------------------------------------- + +==== Limiting Indices to Restore + +By default all indices are restored. With the `indices` property you can +provide a list of indices that should be restored: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-indices] +-------------------------------------------------- +<1> Request that Elasticsearch only restores "test_index". + +==== Renaming Indices + +You can rename indices using regular expressions when restoring a snapshot: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-rename] +-------------------------------------------------- +<1> A regular expression matching the indices that should be renamed. +<2> A replacement pattern that references the group from the regular + expression as `$1`. "test_index" from the snapshot is restored as + "restored_index" in this example. + +==== Index Settings and Options + +You can also customize index settings and options when restoring: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-index-settings] +-------------------------------------------------- +<1> Use `#indexSettings()` to set any specific index setting for the indices + that are restored. +<2> Use `#ignoreIndexSettings()` to provide index settings that should be + ignored from the original indices. +<3> Set `IndicesOptions.Option.IGNORE_UNAVAILABLE` in `#indicesOptions()` to + have the restore succeed even if indices are missing in the snapshot. + +==== Further Arguments + +The following arguments can optionally be provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-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[restore-snapshot-request-waitForCompletion] +-------------------------------------------------- +<1> Boolean indicating whether to wait until the snapshot has been restored. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-partial] +-------------------------------------------------- +<1> Boolean indicating whether the entire snapshot should succeed although one + or more indices participating in the snapshot don’t have all primary + shards available. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-include-global-state] +-------------------------------------------------- +<1> Boolean indicating whether restored templates that don’t currently exist + in the cluster are added and existing templates with the same name are + replaced by the restored templates. The restored persistent settings are + added to the existing persistent settings. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-request-include-aliases] +-------------------------------------------------- +<1> Boolean to control whether aliases should be restored. Set to `false` to + prevent aliases from being restored together with associated indices. + +[[java-rest-high-snapshot-restore-snapshot-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-execute] +-------------------------------------------------- + +[[java-rest-high-snapshot-restore-snapshot-async]] +==== Asynchronous Execution + +The asynchronous execution of a restore snapshot request requires both the +`RestoreSnapshotRequest` instance and an `ActionListener` instance to be +passed to the asynchronous method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-execute-async] +-------------------------------------------------- +<1> The `RestoreSnapshotRequest` 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 using the `onResponse` method +if the execution successfully completed or using the `onFailure` method if +it failed. + +A typical listener for `RestoreSnapshotResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-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-cluster-restore-snapshot-response]] +==== Restore Snapshot Response + +The returned `RestoreSnapshotResponse` allows to retrieve information about the +executed operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[restore-snapshot-response] +-------------------------------------------------- +<1> The `RestoreInfo` contains details about the restored snapshot like the indices or + the number of successfully restored and failed shards. diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java index c1b8c73c9ef..53aa522772a 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequest.java @@ -27,14 +27,17 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; @@ -45,7 +48,7 @@ import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBo /** * Restore snapshot request */ -public class RestoreSnapshotRequest extends MasterNodeRequest { +public class RestoreSnapshotRequest extends MasterNodeRequest implements ToXContentObject { private String snapshot; private String repository; @@ -563,6 +566,49 @@ public class RestoreSnapshotRequest extends MasterNodeRequest PARSER = new ConstructingObjectParser<>( + "restore_snapshot", true, v -> { + RestoreInfo restoreInfo = (RestoreInfo) v[0]; + Boolean accepted = (Boolean) v[1]; + assert (accepted == null && restoreInfo != null) || + (accepted != null && accepted && restoreInfo == null) : + "accepted: [" + accepted + "], restoreInfo: [" + restoreInfo + "]"; + return new RestoreSnapshotResponse(restoreInfo); + }); + + static { + PARSER.declareObject(optionalConstructorArg(), (parser, context) -> RestoreInfo.fromXContent(parser), new ParseField("snapshot")); + PARSER.declareBoolean(optionalConstructorArg(), new ParseField("accepted")); + } + + + public static RestoreSnapshotResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RestoreSnapshotResponse that = (RestoreSnapshotResponse) o; + return Objects.equals(restoreInfo, that.restoreInfo); + } + + @Override + public int hashCode() { + return Objects.hash(restoreInfo); + } + + @Override + public String toString() { + return "RestoreSnapshotResponse{" + "restoreInfo=" + restoreInfo + '}'; + } } diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreInfo.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreInfo.java index 36e80501fc1..6a58b52f72a 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreInfo.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreInfo.java @@ -18,18 +18,22 @@ */ package org.elasticsearch.snapshots; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; -import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.rest.RestStatus; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Information about successfully completed restore operation. @@ -120,9 +124,6 @@ public class RestoreInfo implements ToXContentObject, Streamable { static final String SUCCESSFUL = "successful"; } - /** - * {@inheritDoc} - */ @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -141,9 +142,23 @@ public class RestoreInfo implements ToXContentObject, Streamable { return builder; } - /** - * {@inheritDoc} - */ + private static final ObjectParser PARSER = new ObjectParser<>(RestoreInfo.class.getName(), true, RestoreInfo::new); + + static { + ObjectParser shardsParser = new ObjectParser<>("shards", true, null); + shardsParser.declareInt((r, s) -> r.totalShards = s, new ParseField(Fields.TOTAL)); + shardsParser.declareInt((r, s) -> { /* only consume, don't set */ }, new ParseField(Fields.FAILED)); + shardsParser.declareInt((r, s) -> r.successfulShards = s, new ParseField(Fields.SUCCESSFUL)); + + PARSER.declareString((r, n) -> r.name = n, new ParseField(Fields.SNAPSHOT)); + PARSER.declareStringArray((r, i) -> r.indices = i, new ParseField(Fields.INDICES)); + PARSER.declareField(shardsParser::parse, new ParseField(Fields.SHARDS), ObjectParser.ValueType.OBJECT); + } + + public static RestoreInfo fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + @Override public void readFrom(StreamInput in) throws IOException { name = in.readString(); @@ -157,9 +172,6 @@ public class RestoreInfo implements ToXContentObject, Streamable { successfulShards = in.readVInt(); } - /** - * {@inheritDoc} - */ @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(name); @@ -193,4 +205,24 @@ public class RestoreInfo implements ToXContentObject, Streamable { return in.readOptionalStreamable(RestoreInfo::new); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RestoreInfo that = (RestoreInfo) o; + return totalShards == that.totalShards && + successfulShards == that.successfulShards && + Objects.equals(name, that.name) && + Objects.equals(indices, that.indices); + } + + @Override + public int hashCode() { + return Objects.hash(name, indices, totalShards, successfulShards); + } + + @Override + public String toString() { + return Strings.toString(this); + } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestTests.java new file mode 100644 index 00000000000..fbe8761a07d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotRequestTests.java @@ -0,0 +1,141 @@ +/* + * 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.restore; + +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RestoreSnapshotRequestTests extends AbstractWireSerializingTestCase { + private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) { + if (randomBoolean()) { + List indices = new ArrayList<>(); + int count = randomInt(3) + 1; + + for (int i = 0; i < count; ++i) { + indices.add(randomAlphaOfLength(randomInt(3) + 2)); + } + + instance.indices(indices); + } + if (randomBoolean()) { + instance.renamePattern(randomUnicodeOfLengthBetween(1, 100)); + } + if (randomBoolean()) { + instance.renameReplacement(randomUnicodeOfLengthBetween(1, 100)); + } + instance.partial(randomBoolean()); + instance.includeAliases(randomBoolean()); + + if (randomBoolean()) { + Map settings = new HashMap<>(); + int count = randomInt(3) + 1; + + for (int i = 0; i < count; ++i) { + settings.put(randomAlphaOfLengthBetween(2, 5), randomAlphaOfLengthBetween(2, 5)); + } + + instance.settings(settings); + } + if (randomBoolean()) { + Map indexSettings = new HashMap<>(); + int count = randomInt(3) + 1; + + for (int i = 0; i < count; ++i) { + indexSettings.put(randomAlphaOfLengthBetween(2, 5), randomAlphaOfLengthBetween(2, 5));; + } + instance.indexSettings(indexSettings); + } + + instance.includeGlobalState(randomBoolean()); + + if (randomBoolean()) { + Collection wildcardStates = randomSubsetOf( + Arrays.asList(IndicesOptions.WildcardStates.values())); + Collection options = randomSubsetOf( + Arrays.asList(IndicesOptions.Option.ALLOW_NO_INDICES, IndicesOptions.Option.IGNORE_UNAVAILABLE)); + + instance.indicesOptions(new IndicesOptions( + options.isEmpty() ? IndicesOptions.Option.NONE : EnumSet.copyOf(options), + wildcardStates.isEmpty() ? IndicesOptions.WildcardStates.NONE : EnumSet.copyOf(wildcardStates))); + } + + instance.waitForCompletion(randomBoolean()); + + if (randomBoolean()) { + instance.masterNodeTimeout(randomTimeValue()); + } + return instance; + } + + @Override + protected RestoreSnapshotRequest createTestInstance() { + return randomState(new RestoreSnapshotRequest(randomAlphaOfLength(5), randomAlphaOfLength(10))); + } + + @Override + protected Writeable.Reader instanceReader() { + return RestoreSnapshotRequest::new; + } + + @Override + protected RestoreSnapshotRequest mutateInstance(RestoreSnapshotRequest instance) throws IOException { + RestoreSnapshotRequest copy = copyInstance(instance); + // ensure that at least one property is different + copy.repository("copied-" + instance.repository()); + return randomState(copy); + } + + public void testSource() throws IOException { + RestoreSnapshotRequest original = createTestInstance(); + XContentBuilder builder = original.toXContent(XContentFactory.jsonBuilder(), new ToXContent.MapParams(Collections.emptyMap())); + XContentParser parser = XContentType.JSON.xContent().createParser( + NamedXContentRegistry.EMPTY, null, BytesReference.bytes(builder).streamInput()); + Map map = parser.mapOrdered(); + + // we will only restore properties from the map that are contained in the request body. All other + // properties are restored from the original (in the actual REST action this is restored from the + // REST path and request parameters). + RestoreSnapshotRequest processed = new RestoreSnapshotRequest(original.repository(), original.snapshot()); + processed.masterNodeTimeout(original.masterNodeTimeout()); + processed.waitForCompletion(original.waitForCompletion()); + + processed.source(map); + + assertEquals(original, processed); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotResponseTests.java new file mode 100644 index 00000000000..17d1ecafabd --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotResponseTests.java @@ -0,0 +1,56 @@ +/* + * 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.restore; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.snapshots.RestoreInfo; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class RestoreSnapshotResponseTests extends AbstractXContentTestCase { + + @Override + protected RestoreSnapshotResponse createTestInstance() { + if (randomBoolean()) { + String name = randomRealisticUnicodeOfCodepointLengthBetween(1, 30); + List indices = new ArrayList<>(); + indices.add("test0"); + indices.add("test1"); + int totalShards = randomIntBetween(1, 1000); + int successfulShards = randomIntBetween(0, totalShards); + return new RestoreSnapshotResponse(new RestoreInfo(name, indices, totalShards, successfulShards)); + } else { + return new RestoreSnapshotResponse(null); + } + } + + @Override + protected RestoreSnapshotResponse doParseInstance(XContentParser parser) throws IOException { + return RestoreSnapshotResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 937adddf3a4..e25e8b43d3a 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -536,6 +536,11 @@ public abstract class ESRestTestCase extends ESTestCase { client().performRequest(request); } + protected static void deleteIndex(String name) throws IOException { + Request request = new Request("DELETE", "/" + name); + client().performRequest(request); + } + protected static void updateIndexSettings(String index, Settings.Builder settings) throws IOException { updateIndexSettings(index, settings.build()); }