This is an automated email from the ASF dual-hosted git repository. mhanson pushed a commit to branch support/1.13 in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/support/1.13 by this push: new 9255163 GEODE-8095: Changes to make GEODE Respond to Restore Redundancy REST Command (#5300) (#5327) 9255163 is described below commit 9255163a026d5571a89ba6aad950b75539261414 Author: mhansonp <hans...@vmware.com> AuthorDate: Tue Jun 30 10:35:55 2020 -0700 GEODE-8095: Changes to make GEODE Respond to Restore Redundancy REST Command (#5300) (#5327) * GEODE-8095: Changes to make GEODE Respond to Restore Redundancy (cherry picked from commit f5c5e2cc7860c132074a16351ca4db847f64d6f7) Co-authored-by: Jinmei Liao <jil...@vmware.com> --- .../rest/RestoreRedundancyManagementDUnitTest.java | 345 +++++++++++++++++++++ .../internal/operation/OperationManager.java | 2 + ...rializableRestoreRedundancyResultsImplTest.java | 5 +- ...RedundancyRequestControllerIntegrationTest.java | 174 +++++++++++ .../RestoreRedundancyOperationController.java | 74 +++++ 5 files changed, 596 insertions(+), 4 deletions(-) diff --git a/geode-assembly/src/distributedTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyManagementDUnitTest.java b/geode-assembly/src/distributedTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyManagementDUnitTest.java new file mode 100644 index 0000000..3464fe7 --- /dev/null +++ b/geode-assembly/src/distributedTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyManagementDUnitTest.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + */ + +package org.apache.geode.management.internal.rest; + +import static org.apache.geode.cache.PartitionAttributesFactory.GLOBAL_MAX_BUCKETS_DEFAULT; +import static org.apache.geode.cache.Region.SEPARATOR; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.internal.cache.InternalCache; +import org.apache.geode.internal.cache.PartitionAttributesImpl; +import org.apache.geode.internal.cache.PartitionedRegion; +import org.apache.geode.management.api.ClusterManagementOperationResult; +import org.apache.geode.management.api.ClusterManagementService; +import org.apache.geode.management.client.ClusterManagementServiceBuilder; +import org.apache.geode.management.operation.RestoreRedundancyRequest; +import org.apache.geode.management.runtime.RegionRedundancyStatus; +import org.apache.geode.management.runtime.RestoreRedundancyResults; +import org.apache.geode.test.dunit.rules.ClusterStartupRule; +import org.apache.geode.test.dunit.rules.MemberVM; +import org.apache.geode.test.junit.rules.MemberStarterRule; + +/** + * This class borrows very heavily from the RestoreRedundancyCommandDUnitTest + * + */ + +public class RestoreRedundancyManagementDUnitTest { + + @Rule + public ClusterStartupRule cluster = new ClusterStartupRule(); + + private MemberVM locator; + private List<MemberVM> servers; + private static final int SERVERS_TO_START = 3; + private static final String HIGH_REDUNDANCY_REGION_NAME = "highRedundancy"; + private static final int HIGH_REDUNDANCY_COPIES = SERVERS_TO_START - 1; + private static final String LOW_REDUNDANCY_REGION_NAME = "lowRedundancy"; + private static final String PARENT_REGION_NAME = "colocatedParent"; + private static final String CHILD_REGION_NAME = "colocatedChild"; + private static final int SINGLE_REDUNDANT_COPY = 1; + private static final String NO_CONFIGURED_REDUNDANCY_REGION_NAME = "noConfiguredRedundancy"; + + private ClusterManagementService client1; + + @Before + public void setup() { + locator = cluster.startLocatorVM(0, MemberStarterRule::withHttpService); + servers = new ArrayList<>(); + int locatorPort = locator.getPort(); + IntStream.range(0, SERVERS_TO_START) + .forEach(i -> servers.add(cluster.startServerVM(i + 1, locatorPort))); + + client1 = new ClusterManagementServiceBuilder() + .setHost("localhost") + .setPort(locator.getHttpPort()) + .build(); + } + + @After + public void tearDown() { + client1.close(); + } + + @Test + public void restoreRedundancyWithNoArgumentsRestoresRedundancyForAllRegions() + throws ExecutionException, InterruptedException { + + List<String> regionNames = getAllRegionNames(); + createAndPopulateRegions(regionNames); + + int numberOfServers = servers.size(); + regionNames.forEach(region -> locator + .waitUntilRegionIsReadyOnExactlyThisManyServers(SEPARATOR + region, numberOfServers)); + + RestoreRedundancyRequest restoreRedundancyRequest = new RestoreRedundancyRequest(); + + restoreRedundancyRequest.setIncludeRegions(regionNames); + + verifyClusterManagementOperationRequestAndResponse(restoreRedundancyRequest); + + // Confirm all regions have their configured redundancy and that primaries were balanced + int numberOfActiveServers = servers.size(); + servers.get(0).invoke(() -> { + for (String regionName : regionNames) { + assertRedundancyStatusForRegion(regionName, true); + assertPrimariesBalanced(regionName, numberOfActiveServers, true); + } + }); + } + + // Helper methods + private void verifyClusterManagementOperationRequestAndResponse( + RestoreRedundancyRequest restoreRedundancyRequest) + throws InterruptedException, ExecutionException { + ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> startResult = + client1.start(restoreRedundancyRequest); + + assertThat(startResult.isSuccessful()).isTrue(); + + ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> endResult = + client1.getFuture(restoreRedundancyRequest, startResult.getOperationId()).get(); + RestoreRedundancyResults restoreRedundancyResult = endResult.getOperationResult(); + + assertThat(restoreRedundancyResult.getSuccess()).isTrue(); + + boolean found; + for (String regionName : restoreRedundancyRequest.getIncludeRegions()) { + found = false; + for (Map.Entry<String, RegionRedundancyStatus> region : restoreRedundancyResult + .getSatisfiedRedundancyRegionResults().entrySet()) { + if (region.getValue().getRegionName().compareTo(regionName) == 0) { + found = true; + break; + } + } + assertThat(found) + .describedAs("Satisfied Redundancy List contains region name is true for included") + .isTrue(); + } + + for (String regionName : restoreRedundancyRequest.getIncludeRegions()) { + for (Map.Entry<String, RegionRedundancyStatus> region : restoreRedundancyResult + .getUnderRedundancyRegionResults().entrySet()) { + assertThat(regionName) + .describedAs("One of regions we expect to be satisfied is marked as Under Redundancy") + .isNotEqualTo(region.getValue().getRegionName()); + } + } + + for (String regionName : restoreRedundancyRequest.getIncludeRegions()) { + for (Map.Entry<String, RegionRedundancyStatus> region : restoreRedundancyResult + .getZeroRedundancyRegionResults().entrySet()) { + assertThat(regionName) + .describedAs("One of regions we expect to be satisfied is marked as Zero Redundancy") + .isNotEqualTo(region.getValue().getRegionName()); + } + } + + List<String> filteredExclude; + + if (restoreRedundancyRequest.getExcludeRegions() != null) { + filteredExclude = new ArrayList<>(restoreRedundancyRequest.getExcludeRegions()); + } else { + filteredExclude = new ArrayList<>(); + } + + for (String regionName : restoreRedundancyRequest.getIncludeRegions()) { + filteredExclude.remove(regionName); + } + + // Testing for the absence of the region name... + for (String regionName : filteredExclude) { + found = false; + for (Map.Entry<String, RegionRedundancyStatus> region : restoreRedundancyResult + .getSatisfiedRedundancyRegionResults().entrySet()) { + if (region.getValue().getRegionName().compareTo(regionName) == 0) { + found = true; + break; + } + } + assertThat(found) + .describedAs("Satisfied Redundancy List contains region name is false for excluded") + .isFalse(); + } + } + + private List<String> getAllRegionNames() { + List<String> regionNames = new ArrayList<>(); + regionNames.add(HIGH_REDUNDANCY_REGION_NAME); + regionNames.add(LOW_REDUNDANCY_REGION_NAME); + regionNames.add(PARENT_REGION_NAME); + regionNames.add(CHILD_REGION_NAME); + regionNames.add(NO_CONFIGURED_REDUNDANCY_REGION_NAME); + return regionNames; + } + + private void createAndPopulateRegions(List<String> regionNames) { + // Create regions on server 1 and populate them. + servers.get(0).invoke(() -> { + createRegions(regionNames); + populateRegions(regionNames); + }); + + // Create regions on other servers. Since recovery delay is infinite, all buckets for these + // regions remain on server 1 and redundancy is zero + servers.subList(1, servers.size()).forEach(s -> s.invoke(() -> { + createRegions(regionNames); + })); + + int numberOfActiveServers = servers.size(); + servers.get(0).invoke(() -> { + // Confirm that redundancy is impaired for all regions + for (String regionName : regionNames) { + assertRedundancyStatusForRegion(regionName, false); + assertPrimariesBalanced(regionName, numberOfActiveServers, false); + } + }); + } + + private static void createRegions(List<String> regionsToCreate) { + if (regionsToCreate.contains(HIGH_REDUNDANCY_REGION_NAME)) { + createHighRedundancyRegion(); + } + if (regionsToCreate.contains(LOW_REDUNDANCY_REGION_NAME)) { + createLowRedundancyRegion(); + } + // We have to create both colocated regions or neither + if (regionsToCreate.contains(PARENT_REGION_NAME) + || regionsToCreate.contains(CHILD_REGION_NAME)) { + createColocatedRegions(); + } + if (regionsToCreate.contains(NO_CONFIGURED_REDUNDANCY_REGION_NAME)) { + createNoConfiguredRedundancyRegion(); + } + } + + private static void createHighRedundancyRegion() { + PartitionAttributesImpl attributes = getAttributes(HIGH_REDUNDANCY_COPIES); + InternalCache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + cache.createRegionFactory(RegionShortcut.PARTITION).setPartitionAttributes(attributes) + .create(HIGH_REDUNDANCY_REGION_NAME); + } + + private static void createLowRedundancyRegion() { + PartitionAttributesImpl attributes = getAttributes(SINGLE_REDUNDANT_COPY); + InternalCache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + cache.createRegionFactory(RegionShortcut.PARTITION).setPartitionAttributes(attributes) + .create(LOW_REDUNDANCY_REGION_NAME); + } + + private static void createColocatedRegions() { + PartitionAttributesImpl attributes = getAttributes(SINGLE_REDUNDANT_COPY); + InternalCache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + // Create parent region + cache.createRegionFactory(RegionShortcut.PARTITION).setPartitionAttributes(attributes) + .create(PARENT_REGION_NAME); + + // Create colocated region + attributes.setColocatedWith(PARENT_REGION_NAME); + cache.createRegionFactory(RegionShortcut.PARTITION).setPartitionAttributes(attributes) + .create(CHILD_REGION_NAME); + } + + private static void createNoConfiguredRedundancyRegion() { + PartitionAttributesImpl attributes = getAttributes(0); + InternalCache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + cache.createRegionFactory(RegionShortcut.PARTITION).setPartitionAttributes(attributes) + .create(NO_CONFIGURED_REDUNDANCY_REGION_NAME); + } + + private static PartitionAttributesImpl getAttributes(int redundantCopies) { + PartitionAttributesImpl attributes = new PartitionAttributesImpl(); + attributes.setStartupRecoveryDelay(-1); + attributes.setRecoveryDelay(-1); + attributes.setRedundantCopies(redundantCopies); + return attributes; + } + + private static void populateRegions(List<String> regionNames) { + if (regionNames.isEmpty()) { + return; + } + Cache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + + // Populate all the regions + regionNames.forEach(regionName -> { + Region<Object, Object> region = cache.getRegion(regionName); + IntStream.range(0, 5 * GLOBAL_MAX_BUCKETS_DEFAULT) + .forEach(i -> region.put("key" + i, "value" + i)); + }); + } + + private static void assertRedundancyStatusForRegion(String regionName, + boolean shouldBeSatisfied) { + // Redundancy is always satisfied for a region with zero configured redundancy + if (regionName.equals(NO_CONFIGURED_REDUNDANCY_REGION_NAME)) { + return; + } + Cache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + + PartitionedRegion region = (PartitionedRegion) cache.getRegion(regionName); + Assert.assertThat(region.getRedundancyProvider().isRedundancyImpaired(), + is(!shouldBeSatisfied)); + } + + private static void assertPrimariesBalanced(String regionName, int numberOfServers, + boolean shouldBeBalanced) { + // Primaries cannot be balanced for regions with no redundant copies + if (regionName.equals(NO_CONFIGURED_REDUNDANCY_REGION_NAME)) { + return; + } + Cache cache = Objects.requireNonNull(ClusterStartupRule.getCache()); + + PartitionedRegion region = (PartitionedRegion) cache.getRegion(regionName); + int primariesOnServer = region.getLocalPrimaryBucketsListTestOnly().size(); + // Add one to the expected number of primaries to deal with integer rounding errors + int expectedPrimaries = (GLOBAL_MAX_BUCKETS_DEFAULT / numberOfServers) + 1; + // Because of the way reassigning primaries works, it is sometimes only possible to get the + // difference between the most loaded member and the least loaded member to be 2, not 1 as would + // be the case for perfect balance + String message = "Primaries should be balanced for region " + regionName + + ", but expectedPrimaries:actualPrimaries = " + + expectedPrimaries + ":" + primariesOnServer; + if (shouldBeBalanced) { + Assert.assertThat(message, Math.abs(primariesOnServer - expectedPrimaries), + is(lessThanOrEqualTo(2))); + } else { + Assert.assertThat("Primaries should not be balanced for region " + regionName, + Math.abs(primariesOnServer - expectedPrimaries), is(not(lessThanOrEqualTo(2)))); + } + } +} diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/operation/OperationManager.java b/geode-core/src/main/java/org/apache/geode/management/internal/operation/OperationManager.java index 7aa9aff..c5c7027 100644 --- a/geode-core/src/main/java/org/apache/geode/management/internal/operation/OperationManager.java +++ b/geode-core/src/main/java/org/apache/geode/management/internal/operation/OperationManager.java @@ -25,6 +25,7 @@ import org.apache.geode.internal.cache.InternalCache; import org.apache.geode.logging.internal.executors.LoggingExecutors; import org.apache.geode.management.api.ClusterManagementOperation; import org.apache.geode.management.operation.RebalanceOperation; +import org.apache.geode.management.operation.RestoreRedundancyRequest; import org.apache.geode.management.runtime.OperationResult; @Experimental @@ -42,6 +43,7 @@ public class OperationManager implements AutoCloseable { // initialize the list of operation performers performers = new ConcurrentHashMap<>(); registerOperation(RebalanceOperation.class, new RebalanceOperationPerformer()); + registerOperation(RestoreRedundancyRequest.class, new RestoreRedundancyPerformer()); } /** diff --git a/geode-core/src/test/java/org/apache/geode/internal/cache/control/SerializableRestoreRedundancyResultsImplTest.java b/geode-core/src/test/java/org/apache/geode/internal/cache/control/SerializableRestoreRedundancyResultsImplTest.java index 473ee30..f365c6d 100644 --- a/geode-core/src/test/java/org/apache/geode/internal/cache/control/SerializableRestoreRedundancyResultsImplTest.java +++ b/geode-core/src/test/java/org/apache/geode/internal/cache/control/SerializableRestoreRedundancyResultsImplTest.java @@ -210,14 +210,11 @@ public class SerializableRestoreRedundancyResultsImplTest { regionRedundancyStatus.setStatus(SATISFIED); restoreRedundancyResults.addRegionResult(regionRedundancyStatus); String jsonString = geodeMapper.writeValueAsString(restoreRedundancyResults); - // deserialize the class - + // deserialize the class RestoreRedundancyResultsImpl value = geodeMapper.readValue(jsonString, RestoreRedundancyResultsImpl.class); assertThat(value).usingRecursiveComparison().isEqualTo(restoreRedundancyResults); - - } } diff --git a/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyRequestControllerIntegrationTest.java b/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyRequestControllerIntegrationTest.java new file mode 100644 index 0000000..1c7fbc0 --- /dev/null +++ b/geode-web-management/src/integrationTest/java/org/apache/geode/management/internal/rest/RestoreRedundancyRequestControllerIntegrationTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + */ + +package org.apache.geode.management.internal.rest; + +import static org.apache.geode.management.configuration.Links.URI_VERSION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.StringContains.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.concurrent.CompletableFuture; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +import org.apache.geode.management.api.ClusterManagementListOperationsResult; +import org.apache.geode.management.api.ClusterManagementOperationResult; +import org.apache.geode.management.api.ClusterManagementService; +import org.apache.geode.management.api.ClusterManagementServiceTransport; +import org.apache.geode.management.api.RestTemplateClusterManagementServiceTransport; +import org.apache.geode.management.client.ClusterManagementServiceBuilder; +import org.apache.geode.management.operation.RestoreRedundancyRequest; +import org.apache.geode.management.runtime.RestoreRedundancyResults; + +@RunWith(SpringRunner.class) +@ContextConfiguration(locations = {"classpath*:WEB-INF/management-servlet.xml"}, + loader = PlainLocatorContextLoader.class) +@WebAppConfiguration +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class RestoreRedundancyRequestControllerIntegrationTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + // needs to be used together with any LocatorContextLoader + private LocatorWebContext context; + + private ClusterManagementService client; + private static final String RESTORE_REDUNDANCY_URL = "/operations/restoreRedundancy"; + + @Before + public void before() { + context = new LocatorWebContext(webApplicationContext); + + RestTemplate template = new RestTemplate(); + template.setRequestFactory(context.getRequestFactory()); + ClusterManagementServiceTransport transport = + new RestTemplateClusterManagementServiceTransport(template); + client = new ClusterManagementServiceBuilder().setTransport(transport).build(); + } + + @Test + public void start() throws Exception { + String json = "{}"; + context.perform(post("/v1" + RESTORE_REDUNDANCY_URL).content(json)) + .andExpect(status().isAccepted()) + .andExpect(content().string(not(containsString("\"class\"")))) + .andExpect( + jsonPath("$.links.self", + Matchers.containsString("/v1" + RESTORE_REDUNDANCY_URL))) + .andExpect(jsonPath("$.statusMessage", Matchers.containsString("Operation started"))); + } + + @Test + public void getStatus() throws Exception { + String json = "{}"; + CompletableFuture<String> futureUri = new CompletableFuture<>(); + context.perform(post("/v1" + RESTORE_REDUNDANCY_URL).content(json)) + .andExpect(status().isAccepted()) + .andExpect(new ResponseBodyMatchers().containsObjectAsJson(futureUri)) + .andExpect(jsonPath("$.statusMessage", Matchers.containsString("Operation started"))); + while (true) { + try { + ResultActions resultActions = context.perform(get(futureUri.get())); + resultActions + .andExpect(status().isOk()) + .andExpect(jsonPath("$.statusCode", Matchers.is("IN_PROGRESS"))); + } catch (AssertionError t) { + context.perform(get(futureUri.get())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.operationResult.success", Matchers.is(true))); + return; + } + } + } + + static class ResponseBodyMatchers { + ResultMatcher containsObjectAsJson(CompletableFuture<String> futureUri) { + return mvcResult -> { + String json = mvcResult.getResponse().getContentAsString(); + String uri = + json.replaceFirst(".*\"self\":\"[^\"]*" + URI_VERSION, URI_VERSION).replaceFirst("\".*", + ""); + futureUri.complete(uri); + }; + } + } + + @Test + public void checkStatusOperationDoesNotExist() throws Exception { + context.perform(get("/v1" + RESTORE_REDUNDANCY_URL + "/abc")) + .andExpect(status().isNotFound()) + .andExpect(content().string(not(containsString("\"class\"")))) + .andExpect(jsonPath("$.statusCode", Matchers.is("ENTITY_NOT_FOUND"))) + .andExpect( + jsonPath("$.statusMessage", + Matchers.containsString("Operation 'abc' does not exist."))); + } + + @Test + public void list() throws Exception { + String json = "{}"; + context.perform(post("/v1" + RESTORE_REDUNDANCY_URL).content(json)); + context.perform(get("/v1" + RESTORE_REDUNDANCY_URL)) + .andExpect(status().isOk()) + .andExpect(content().string(not(containsString("\"class\"")))) + .andExpect( + jsonPath("$.result[0].statusCode", Matchers.isOneOf("IN_PROGRESS", "ERROR", "OK"))) + .andExpect( + jsonPath("$.result[0].links.self", Matchers.containsString("restoreRedundancy/"))) + .andExpect(jsonPath("$.statusCode", Matchers.is("OK"))); + } + + @Test + public void doOperation() throws Exception { + RestoreRedundancyRequest rebalance = + new RestoreRedundancyRequest(); + ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> result = + client.start(rebalance); + assertThat(result.isSuccessful()).isTrue(); + assertThat(result.getStatusMessage()) + .isEqualTo("Operation started. Use the URI to check its status."); + } + + @Test + public void doListOperations() { + client.start(new RestoreRedundancyRequest()); + ClusterManagementListOperationsResult<RestoreRedundancyRequest, RestoreRedundancyResults> listResult = + client.list(new RestoreRedundancyRequest()); + assertThat(listResult.getResult().size()).isGreaterThanOrEqualTo(1); + assertThat(listResult.getResult().get(0).getOperationStart()).isNotNull(); + assertThat(listResult.getResult().get(0).getStatusCode().toString()).isIn("IN_PROGRESS", + "ERROR", "OK"); + } +} diff --git a/geode-web-management/src/main/java/org/apache/geode/management/internal/rest/controllers/RestoreRedundancyOperationController.java b/geode-web-management/src/main/java/org/apache/geode/management/internal/rest/controllers/RestoreRedundancyOperationController.java new file mode 100644 index 0000000..5f1ece4 --- /dev/null +++ b/geode-web-management/src/main/java/org/apache/geode/management/internal/rest/controllers/RestoreRedundancyOperationController.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * + */ + +package org.apache.geode.management.internal.rest.controllers; + +import static org.apache.geode.management.configuration.Links.URI_VERSION; +import static org.apache.geode.management.operation.RestoreRedundancyRequest.RESTORE_REDUNDANCY_ENDPOINT; + +import java.util.Optional; + +import io.swagger.annotations.ApiOperation; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.geode.internal.security.SecurityService; +import org.apache.geode.management.api.ClusterManagementListOperationsResult; +import org.apache.geode.management.api.ClusterManagementOperationResult; +import org.apache.geode.management.operation.RestoreRedundancyRequest; +import org.apache.geode.management.runtime.RestoreRedundancyResults; + +@RestController("restoreRedundancyOperation") +@RequestMapping(URI_VERSION) +public class RestoreRedundancyOperationController extends AbstractManagementController { + @ApiOperation(value = "start restore-redundancy") + @PreAuthorize("@securityService.authorize('DATA', 'MANAGE')") + @PostMapping(RESTORE_REDUNDANCY_ENDPOINT) + public ResponseEntity<ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults>> startRestoreRedundancy( + @RequestBody RestoreRedundancyRequest operation) { + operation.setOperator( + Optional.ofNullable(securityService).map(SecurityService::getSubject).map(Object::toString) + .orElse(null)); + ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> result = + clusterManagementService + .start(operation); + return new ResponseEntity<>(result, HttpStatus.ACCEPTED); + } + + @ApiOperation(value = "list restore-redundancy") + @PreAuthorize("@securityService.authorize('DATA', 'MANAGE')") + @GetMapping(RESTORE_REDUNDANCY_ENDPOINT) + public ClusterManagementListOperationsResult<RestoreRedundancyRequest, RestoreRedundancyResults> listRestoreRedundancies() { + return clusterManagementService.list(new RestoreRedundancyRequest()); + } + + @ApiOperation(value = "get restore-redundancy") + @PreAuthorize("@securityService.authorize('DATA', 'MANAGE')") + @GetMapping(RESTORE_REDUNDANCY_ENDPOINT + "/{id:.+}") + public ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> getRestoreRedundancy( + @PathVariable String id) { + ClusterManagementOperationResult<RestoreRedundancyRequest, RestoreRedundancyResults> result = + clusterManagementService.get(new RestoreRedundancyRequest(), id); + return result; + } +}