Browse Source

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
Daniel Mitterdorfer 7 years ago
parent
commit
73a38895fd

+ 15 - 0
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())

+ 32 - 0
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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
+     * API on elastic.co</a>
+     *
+     * @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 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
+     * API on elastic.co</a>
+     *
+     * @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<RestoreSnapshotResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(restoreSnapshotRequest, RequestConverters::restoreSnapshot, options,
+            RestoreSnapshotResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Deletes a snapshot.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore

+ 26 - 0
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<String, String> 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<String, String> expectedParams = new HashMap<>();
         String repository = randomIndicesNames(1, 1)[0];

+ 0 - 1
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"

+ 41 - 0
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";

+ 106 - 0
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<String> 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<RestoreSnapshotResponse> listener =
+                new ActionListener<RestoreSnapshotResponse>() {
+                    @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();
 

+ 144 - 0
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.

+ 80 - 1
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<RestoreSnapshotRequest> {
+public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotRequest> implements ToXContentObject {
 
     private String snapshot;
     private String repository;
@@ -563,6 +566,49 @@ public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotReq
         return this;
     }
 
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.startArray("indices");
+        for (String index : indices) {
+            builder.value(index);
+        }
+        builder.endArray();
+        if (indicesOptions != null) {
+            indicesOptions.toXContent(builder, params);
+        }
+        if (renamePattern != null) {
+            builder.field("rename_pattern", renamePattern);
+        }
+        if (renameReplacement != null) {
+            builder.field("rename_replacement", renameReplacement);
+        }
+        builder.field("include_global_state", includeGlobalState);
+        builder.field("partial", partial);
+        builder.field("include_aliases", includeAliases);
+        if (settings != null) {
+            builder.startObject("settings");
+            if (settings.isEmpty() == false) {
+                settings.toXContent(builder, params);
+            }
+            builder.endObject();
+        }
+        if (indexSettings != null) {
+            builder.startObject("index_settings");
+            if (indexSettings.isEmpty() == false) {
+                indexSettings.toXContent(builder, params);
+            }
+            builder.endObject();
+        }
+        builder.startArray("ignore_index_settings");
+        for (String ignoreIndexSetting : ignoreIndexSettings) {
+            builder.value(ignoreIndexSetting);
+        }
+        builder.endArray();
+        builder.endObject();
+        return builder;
+    }
+
     @Override
     public void readFrom(StreamInput in) throws IOException {
         throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
@@ -573,4 +619,37 @@ public class RestoreSnapshotRequest extends MasterNodeRequest<RestoreSnapshotReq
         return "snapshot [" + repository + ":" + snapshot + "]";
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        RestoreSnapshotRequest that = (RestoreSnapshotRequest) o;
+        return waitForCompletion == that.waitForCompletion &&
+            includeGlobalState == that.includeGlobalState &&
+            partial == that.partial &&
+            includeAliases == that.includeAliases &&
+            Objects.equals(snapshot, that.snapshot) &&
+            Objects.equals(repository, that.repository) &&
+            Arrays.equals(indices, that.indices) &&
+            Objects.equals(indicesOptions, that.indicesOptions) &&
+            Objects.equals(renamePattern, that.renamePattern) &&
+            Objects.equals(renameReplacement, that.renameReplacement) &&
+            Objects.equals(settings, that.settings) &&
+            Objects.equals(indexSettings, that.indexSettings) &&
+            Arrays.equals(ignoreIndexSettings, that.ignoreIndexSettings);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(snapshot, repository, indicesOptions, renamePattern, renameReplacement, waitForCompletion,
+            includeGlobalState, partial, includeAliases, settings, indexSettings);
+        result = 31 * result + Arrays.hashCode(indices);
+        result = 31 * result + Arrays.hashCode(ignoreIndexSettings);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
 }

+ 44 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/restore/RestoreSnapshotResponse.java

@@ -21,15 +21,21 @@ package org.elasticsearch.action.admin.cluster.snapshots.restore;
 
 import org.elasticsearch.action.ActionResponse;
 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.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.snapshots.RestoreInfo;
 
 import java.io.IOException;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
 /**
  * Contains information about restores snapshot
@@ -86,4 +92,42 @@ public class RestoreSnapshotResponse extends ActionResponse implements ToXConten
         builder.endObject();
         return builder;
     }
+
+    public static final ConstructingObjectParser<RestoreSnapshotResponse, Void> 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 + '}';
+    }
 }

+ 42 - 10
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<RestoreInfo, Void> PARSER = new ObjectParser<>(RestoreInfo.class.getName(), true, RestoreInfo::new);
+
+    static {
+        ObjectParser<RestoreInfo, Void> 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);
+    }
 }

+ 141 - 0
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<RestoreSnapshotRequest> {
+    private RestoreSnapshotRequest randomState(RestoreSnapshotRequest instance) {
+        if (randomBoolean()) {
+            List<String> 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<String, Object> 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<String, Object> 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<IndicesOptions.WildcardStates> wildcardStates = randomSubsetOf(
+                Arrays.asList(IndicesOptions.WildcardStates.values()));
+            Collection<IndicesOptions.Option> 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<RestoreSnapshotRequest> 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<String, Object> 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);
+    }
+}

+ 56 - 0
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<RestoreSnapshotResponse> {
+
+    @Override
+    protected RestoreSnapshotResponse createTestInstance() {
+        if (randomBoolean()) {
+            String name = randomRealisticUnicodeOfCodepointLengthBetween(1, 30);
+            List<String> 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;
+    }
+}

+ 5 - 0
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());
     }