Check cluster heath before setup-passwords (elastic/x-pack-elasticsearch#4104)

Trying to setup passwords on a red cluster (or a cluster that cannot
reach a quorum) is generally not a good idea.

This commit:
- Adds a check for RED cluster status
- Prompts to confirm execution if the cluster is red
- Prints out the reason/type is an error response is received
- Increases the HTTP read timeout so that master election failures are
  reported correctly.

Original commit: elastic/x-pack-elasticsearch@4ffbda23db
This commit is contained in:
Tim Vernum 2018-03-20 17:03:17 +10:00 committed by GitHub
parent bc95ad80ce
commit 7cd5e1d516
3 changed files with 150 additions and 41 deletions

View File

@ -49,6 +49,14 @@ import static org.elasticsearch.xpack.core.security.SecurityField.setting;
public class CommandLineHttpClient {
public static final String HTTP_SSL_SETTING = setting("http.ssl.");
/**
* Timeout HTTP(s) reads after 35 seconds.
* The default timeout for discovering a master is 30s, and we want to be longer than this, otherwise a querying a disconnected node
* will trigger as client side timeout rather than giving clear error details.
*/
private static final int READ_TIMEOUT = 35 * 1000;
private final Settings settings;
private final Environment env;
@ -91,7 +99,7 @@ public class CommandLineHttpClient {
conn = (HttpURLConnection) url.openConnection();
}
conn.setRequestMethod(method);
conn.setReadTimeout(30 * 1000); // 30 second timeout
conn.setReadTimeout(READ_TIMEOUT);
// Add basic-auth header
String token = UsernamePasswordToken.basicAuthHeaderValue(user, password);
conn.setRequestProperty("Authorization", token);

View File

@ -35,7 +35,6 @@ import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
@ -48,6 +47,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Arrays.asList;
@ -80,7 +80,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
}
SetupPasswordTool(CheckedFunction<Environment, CommandLineHttpClient, Exception> clientFunction,
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction) {
super("Sets the passwords for reserved users");
subcommands.put("auto", newAutoSetup());
subcommands.put("interactive", newInteractiveSetup());
@ -120,6 +120,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
setupOptions(options, env);
checkElasticKeystorePasswordValid(terminal, env);
checkClusterHealth(terminal);
if (shouldPrompt) {
terminal.println("Initiating the setup of passwords for reserved users " + String.join(",", USERS) + ".");
@ -165,6 +166,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
terminal.println(Verbosity.VERBOSE, "Running with configuration path: " + env.configFile());
setupOptions(options, env);
checkElasticKeystorePasswordValid(terminal, env);
checkClusterHealth(terminal);
if (shouldPrompt) {
terminal.println("Initiating the setup of passwords for reserved users " + String.join(",", USERS) + ".");
@ -262,8 +264,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
* '_authenticate' call. Returns silently if server is reachable and password is
* valid. Throws {@link UserException} otherwise.
*
* @param terminal
* where to write verbose info.
* @param terminal where to write verbose info.
*/
void checkElasticKeystorePasswordValid(Terminal terminal, Environment env) throws Exception {
URL route = createURL(url, "/_xpack/security/_authenticate", "?pretty");
@ -373,13 +374,53 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to determine x-pack security feature configuration.");
}
void checkClusterHealth(Terminal terminal) throws Exception {
URL route = createURL(url, "/_cluster/health", "?pretty");
terminal.println(Verbosity.VERBOSE, "");
terminal.println(Verbosity.VERBOSE, "Checking cluster health: " + route.toString());
final HttpResponse httpResponse = client.execute("GET", route, elasticUser, elasticUserPassword, () -> null,
is -> responseBuilder(is, terminal));
if (httpResponse.getHttpStatus() != HttpURLConnection.HTTP_OK) {
terminal.println("");
terminal.println("Failed to determine the health of the cluster running at " + url);
terminal.println("Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling GET " + route.toString());
final String cause = getErrorCause(httpResponse);
if (cause != null) {
terminal.println("Cause: " + cause);
}
} else {
final String clusterStatus = Objects.toString(httpResponse.getResponseBody().get("status"), "");
if (clusterStatus.isEmpty()) {
terminal.println("");
terminal.println("Failed to determine the health of the cluster running at " + url);
terminal.println("Could not find a 'status' value at " + route.toString());
} else if ("red".equalsIgnoreCase(clusterStatus)) {
terminal.println("");
terminal.println("Your cluster health is currently RED.");
terminal.println("This means that some cluster data is unavailable and your cluster is not fully functional.");
} else {
// Cluster is yellow/green -> all OK
return;
}
}
terminal.println("");
terminal.println("It is recommended that you resolve the issues with your cluster before running setup-passwords.");
terminal.println("It is very likely that the password changes will fail when run against an unhealthy cluster.");
terminal.println("");
if (shouldPrompt) {
final boolean keepGoing = terminal.promptYesNo("Do you want to continue with the password setup process", false);
if (keepGoing == false) {
throw new UserException(ExitCodes.OK, "User cancelled operation");
}
terminal.println("");
}
}
/**
* Sets one user's password using the elastic superUser credentials.
*
* @param user
* The user who's password will change.
* @param password
* the new password of the user.
* @param user The user who's password will change.
* @param password the new password of the user.
*/
private void changeUserPassword(String user, SecureString password, Terminal terminal) throws Exception {
URL route = createURL(url, "/_xpack/security/user/" + user + "/_password", "?pretty");
@ -401,8 +442,14 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
terminal.println("");
terminal.println(
"Unexpected response code [" + httpResponse.getHttpStatus() + "] from calling PUT " + route.toString());
String cause = getErrorCause(httpResponse);
if (cause != null) {
terminal.println("Cause: " + cause);
terminal.println("");
}
terminal.println("Possible next steps:");
terminal.println("* Try running this tool again.");
terminal.println("* Try running with the --verbose parameter for additional messages.");
terminal.println("* Check the elasticsearch logs for additional error details.");
terminal.println("* Use the change password API manually. ");
terminal.println("");
@ -423,13 +470,11 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
* Collects passwords for all the users, then issues set requests. Fails on the
* first failed request. In this case rerun the tool to redo all the operations.
*
* @param passwordFn
* Function to generate or prompt for each user's password.
* @param successCallback
* Callback for each successful operation
* @param passwordFn Function to generate or prompt for each user's password.
* @param successCallback Callback for each successful operation
*/
void changePasswords(CheckedFunction<String, SecureString, UserException> passwordFn,
CheckedBiConsumer<String, SecureString, Exception> successCallback, Terminal terminal) throws Exception {
CheckedBiConsumer<String, SecureString, Exception> successCallback, Terminal terminal) throws Exception {
Map<String, SecureString> passwordsMap = new HashMap<>(USERS.size());
try {
for (String user : USERS) {
@ -473,6 +518,32 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
}
}
private String getErrorCause(HttpResponse httpResponse) {
final Object error = httpResponse.getResponseBody().get("error");
if (error == null) {
return null;
}
if (error instanceof Map) {
Object reason = ((Map) error).get("reason");
if (reason != null) {
return reason.toString();
}
final Object root = ((Map) error).get("root_cause");
if (root != null && root instanceof Map) {
reason = ((Map) root).get("reason");
if (reason != null) {
return reason.toString();
}
final Object type = ((Map) root).get("type");
if (type != null) {
return (String) type;
}
}
return String.valueOf(((Map) error).get("type"));
}
return error.toString();
}
private static URL createURL(URL url, String path, String query) throws MalformedURLException, URISyntaxException {
URL route = new URL(url, (url.toURI().getPath() + path).replaceAll("/+", "/") + query);
return route;
@ -484,6 +555,7 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand {
static class XPackSecurityFeatureConfig {
final boolean isAvailable;
final boolean isEnabled;
XPackSecurityFeatureConfig(boolean isAvailable, boolean isEnabled) {
this.isAvailable = isAvailable;
this.isEnabled = isEnabled;

View File

@ -12,6 +12,7 @@ import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.CheckedSupplier;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.common.settings.SecureString;
@ -35,6 +36,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser;
import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm;
import org.elasticsearch.xpack.security.authc.esnative.tool.HttpResponse.HttpResponseBuilder;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
@ -42,6 +44,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
@ -56,8 +59,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.SSLException;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
@ -80,7 +81,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void setSecretsAndKeyStore() throws Exception {
// sometimes we fall back to the keystore seed as this is the default when a new node starts
boolean useFallback = randomBoolean();
bootstrapPassword = useFallback ? new SecureString("0xCAFEBABE".toCharArray()) :
bootstrapPassword = useFallback ? new SecureString("0xCAFEBABE".toCharArray()) :
new SecureString("bootstrap-password".toCharArray());
this.keyStore = mock(KeyStoreWrapper.class);
this.httpClient = mock(CommandLineHttpClient.class);
@ -102,6 +103,9 @@ public class SetupPasswordToolTests extends CommandTestCase {
any(CheckedFunction.class))).thenReturn(httpResponse);
URL url = new URL(httpClient.getDefaultURL());
httpResponse = new HttpResponse(HttpURLConnection.HTTP_OK, Collections.singletonMap("status", randomFrom("yellow", "green")));
when(httpClient.execute(anyString(), eq(clusterHealthUrl(url)), anyString(), any(SecureString.class), any(CheckedSupplier.class),
any(CheckedFunction.class))).thenReturn(httpResponse);
URL xpackSecurityPluginQueryURL = queryXPackSecurityFeatureConfigURL(url);
HttpResponse queryXPackSecurityConfigHttpResponse = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<String, Object>());
@ -132,7 +136,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
@Override
protected Environment createEnv(Map<String, String> settings) throws UserException {
Settings.Builder builder = Settings.builder();
settings.forEach((k,v) -> builder.put(k, v));
settings.forEach((k, v) -> builder.put(k, v));
return TestEnvironment.newEnvironment(builder.build());
}
};
@ -144,7 +148,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
@Override
protected Environment createEnv(Map<String, String> settings) throws UserException {
Settings.Builder builder = Settings.builder();
settings.forEach((k,v) -> builder.put(k, v));
settings.forEach((k, v) -> builder.put(k, v));
return TestEnvironment.newEnvironment(builder.build());
}
};
@ -166,11 +170,11 @@ public class SetupPasswordToolTests extends CommandTestCase {
InOrder inOrder = Mockito.inOrder(httpClient);
URL checkUrl = checkURL(url);
URL checkUrl = authenticateUrl(url);
inOrder.verify(httpClient).execute(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
any(CheckedFunction.class));
for (String user : usersInSetOrder) {
URL urlWithRoute = passwdURL(url, user);
URL urlWithRoute = passwordUrl(url, user);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
any(CheckedSupplier.class), any(CheckedFunction.class));
}
@ -178,7 +182,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testAuthnFail() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_UNAUTHORIZED, new HashMap<String, Object>());
@ -195,7 +199,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testErrorMessagesWhenXPackIsNotAvailableOnNode() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_FOUND, new HashMap<String, Object>());
when(httpClient.execute(eq("GET"), eq(authnURL), eq(ElasticUser.NAME), any(SecureString.class), any(CheckedSupplier.class),
@ -214,7 +218,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
when(httpClient.execute(eq("GET"), eq(xpackSecurityPluginQueryURL), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class)))
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_BAD_REQUEST, securityPluginQueryResponseBody));
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_BAD_REQUEST, securityPluginQueryResponseBody));
thrown.expect(UserException.class);
thrown.expectMessage("X-Pack is not available on this Elasticsearch node.");
@ -223,7 +227,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testErrorMessagesWhenXPackIsAvailableWithCorrectLicenseAndIsEnabledButStillFailedForUnknown() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_FOUND, new HashMap<String, Object>());
when(httpClient.execute(eq("GET"), eq(authnURL), eq(ElasticUser.NAME), any(SecureString.class), any(CheckedSupplier.class),
@ -245,7 +249,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
when(httpClient.execute(eq("GET"), eq(xpackSecurityPluginQueryURL), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class)))
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
thrown.expect(UserException.class);
thrown.expectMessage("Unknown error");
@ -255,7 +259,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testErrorMessagesWhenXPackPluginIsAvailableButNoSecurityLicense() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
URL xpackSecurityPluginQueryURL = queryXPackSecurityFeatureConfigURL(url);
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_FOUND, new HashMap<String, Object>());
@ -276,7 +280,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
when(httpClient.execute(eq("GET"), eq(xpackSecurityPluginQueryURL), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class)))
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
thrown.expect(UserException.class);
thrown.expectMessage("X-Pack Security is not available.");
@ -286,7 +290,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testErrorMessagesWhenXPackPluginIsAvailableWithValidLicenseButDisabledSecurity() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
URL xpackSecurityPluginQueryURL = queryXPackSecurityFeatureConfigURL(url);
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_NOT_FOUND, new HashMap<String, Object>());
@ -307,17 +311,16 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
when(httpClient.execute(eq("GET"), eq(xpackSecurityPluginQueryURL), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class)))
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
.thenReturn(createHttpResponse(HttpURLConnection.HTTP_OK, securityPluginQueryResponseBody));
thrown.expect(UserException.class);
thrown.expectMessage("X-Pack Security is disabled by configuration.");
execute(randomBoolean() ? "auto" : "interactive", pathHomeParameter);
}
public void testWrongServer() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
URL authnURL = checkURL(url);
URL authnURL = authenticateUrl(url);
doThrow(randomFrom(new IOException(), new SSLException(""))).when(httpClient).execute(eq("GET"), eq(authnURL), eq(ElasticUser.NAME),
any(SecureString.class), any(CheckedSupplier.class), any(CheckedFunction.class));
@ -329,17 +332,39 @@ public class SetupPasswordToolTests extends CommandTestCase {
}
}
public void testRedCluster() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
HttpResponse httpResponse = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>());
when(httpClient.execute(eq("GET"), eq(authenticateUrl(url)), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class))).thenReturn(httpResponse);
httpResponse = new HttpResponse(HttpURLConnection.HTTP_OK, MapBuilder.<String, Object>newMapBuilder()
.put("cluster_name", "elasticsearch").put("status", "red").put("number_of_nodes", 1).map());
when(httpClient.execute(eq("GET"), eq(clusterHealthUrl(url)), eq(ElasticUser.NAME), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class))).thenReturn(httpResponse);
terminal.addTextInput("n");
try {
execute(randomBoolean() ? "auto" : "interactive", pathHomeParameter);
fail("Should have thrown exception");
} catch (UserException e) {
assertEquals(ExitCodes.OK, e.exitCode);
assertThat(terminal.getOutput(), Matchers.containsString("Your cluster health is currently RED."));
}
}
public void testUrlOption() throws Exception {
URL url = new URL("http://localhost:9202" + randomFrom("", "/", "//", "/smth", "//smth/", "//x//x/"));
execute("auto", pathHomeParameter, "-u", url.toString(), "-b");
InOrder inOrder = Mockito.inOrder(httpClient);
URL checkUrl = checkURL(url);
URL checkUrl = authenticateUrl(url);
inOrder.verify(httpClient).execute(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
any(CheckedFunction.class));
for (String user : usersInSetOrder) {
URL urlWithRoute = passwdURL(url, user);
URL urlWithRoute = passwordUrl(url, user);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
any(CheckedSupplier.class), any(CheckedFunction.class));
}
@ -348,7 +373,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
public void testSetUserPassFail() throws Exception {
URL url = new URL(httpClient.getDefaultURL());
String userToFail = randomFrom(SetupPasswordTool.USERS);
URL userToFailURL = passwdURL(url, userToFail);
URL userToFailURL = passwordUrl(url, userToFail);
doThrow(new IOException()).when(httpClient).execute(eq("PUT"), eq(userToFailURL), anyString(), any(SecureString.class),
any(CheckedSupplier.class), any(CheckedFunction.class));
@ -368,11 +393,11 @@ public class SetupPasswordToolTests extends CommandTestCase {
InOrder inOrder = Mockito.inOrder(httpClient);
URL checkUrl = checkURL(url);
URL checkUrl = authenticateUrl(url);
inOrder.verify(httpClient).execute(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
any(CheckedFunction.class));
for (String user : usersInSetOrder) {
URL urlWithRoute = passwdURL(url, user);
URL urlWithRoute = passwordUrl(url, user);
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
passwordCaptor.capture(), any(CheckedFunction.class));
@ -406,11 +431,11 @@ public class SetupPasswordToolTests extends CommandTestCase {
InOrder inOrder = Mockito.inOrder(httpClient);
URL checkUrl = checkURL(url);
URL checkUrl = authenticateUrl(url);
inOrder.verify(httpClient).execute(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
any(CheckedFunction.class));
for (String user : usersInSetOrder) {
URL urlWithRoute = passwdURL(url, user);
URL urlWithRoute = passwordUrl(url, user);
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
inOrder.verify(httpClient).execute(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
passwordCaptor.capture(), any(CheckedFunction.class));
@ -433,14 +458,18 @@ public class SetupPasswordToolTests extends CommandTestCase {
throw new RuntimeException("Did not properly parse password.");
}
private URL checkURL(URL url) throws MalformedURLException, URISyntaxException {
private URL authenticateUrl(URL url) throws MalformedURLException, URISyntaxException {
return new URL(url, (url.toURI().getPath() + "/_xpack/security/_authenticate").replaceAll("/+", "/") + "?pretty");
}
private URL passwdURL(URL url, String user) throws MalformedURLException, URISyntaxException {
private URL passwordUrl(URL url, String user) throws MalformedURLException, URISyntaxException {
return new URL(url, (url.toURI().getPath() + "/_xpack/security/user/" + user + "/_password").replaceAll("/+", "/") + "?pretty");
}
private URL clusterHealthUrl(URL url) throws MalformedURLException, URISyntaxException {
return new URL(url, (url.toURI().getPath() + "/_cluster/health").replaceAll("/+", "/") + "?pretty");
}
private URL queryXPackSecurityFeatureConfigURL(URL url) throws MalformedURLException, URISyntaxException {
return new URL(url,
(url.toURI().getPath() + "/_xpack").replaceAll("/+", "/") + "?categories=features&human=false&pretty");