Merge remote-tracking branch 'upstream/master'
Original commit: elastic/x-pack-elasticsearch@209befd1b1
This commit is contained in:
commit
a7e0dfb3ec
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
package org.elasticsearch.xpack.security.authc.esnative.tool;
|
||||
|
||||
import org.bouncycastle.util.io.Streams;
|
||||
import org.elasticsearch.common.CheckedConsumer;
|
||||
import org.elasticsearch.common.CheckedSupplier;
|
||||
import org.elasticsearch.common.Strings;
|
||||
|
|
|
@ -7,8 +7,8 @@ package org.elasticsearch.xpack.security.authc.esnative.tool;
|
|||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -21,6 +21,7 @@ import joptsimple.OptionParser;
|
|||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
import org.bouncycastle.util.io.Streams;
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.cli.EnvironmentAwareCommand;
|
||||
import org.elasticsearch.cli.ExitCodes;
|
||||
import org.elasticsearch.cli.MultiCommand;
|
||||
|
@ -113,7 +114,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
checkElasticKeystorePasswordValid(terminal, env);
|
||||
|
||||
if (shouldPrompt) {
|
||||
terminal.println("Initiating the setup of reserved user " + String.join(",", USERS) + " passwords.");
|
||||
terminal.println("Initiating the setup of passwords for reserved users " + String.join(",", USERS) + ".");
|
||||
terminal.println("The passwords will be randomly generated and printed to the console.");
|
||||
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
||||
terminal.println("\n");
|
||||
|
@ -124,7 +125,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
changePasswords((user) -> generatePassword(secureRandom, user),
|
||||
(user, password) -> changedPasswordCallback(terminal, user, password));
|
||||
(user, password) -> changedPasswordCallback(terminal, user, password), terminal);
|
||||
}
|
||||
|
||||
private SecureString generatePassword(SecureRandom secureRandom, String user) {
|
||||
|
@ -158,7 +159,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
checkElasticKeystorePasswordValid(terminal, env);
|
||||
|
||||
if (shouldPrompt) {
|
||||
terminal.println("Initiating the setup of reserved user " + String.join(",", USERS) + " passwords.");
|
||||
terminal.println("Initiating the setup of passwords for reserved users " + String.join(",", USERS) + ".");
|
||||
terminal.println("You will be prompted to enter passwords as the process progresses.");
|
||||
boolean shouldContinue = terminal.promptYesNo("Please confirm that you would like to continue", false);
|
||||
terminal.println("\n");
|
||||
|
@ -168,7 +169,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
}
|
||||
|
||||
changePasswords(user -> promptForPassword(terminal, user),
|
||||
(user, password) -> changedPasswordCallback(terminal, user, password));
|
||||
(user, password) -> changedPasswordCallback(terminal, user, password), terminal);
|
||||
}
|
||||
|
||||
private SecureString promptForPassword(Terminal terminal, String user) throws UserException {
|
||||
|
@ -212,7 +213,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
|
||||
private String elasticUser = ElasticUser.NAME;
|
||||
private SecureString elasticUserPassword;
|
||||
private String url;
|
||||
private URL url;
|
||||
|
||||
SetupCommand(String description) {
|
||||
super(description);
|
||||
|
@ -223,7 +224,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
client = clientFunction.apply(env);
|
||||
try (KeyStoreWrapper keyStore = keyStoreFunction.apply(env)) {
|
||||
String providedUrl = urlOption.value(options);
|
||||
url = providedUrl == null ? client.getDefaultURL() : providedUrl;
|
||||
url = new URL(providedUrl == null ? client.getDefaultURL() : providedUrl);
|
||||
setShouldPrompt(options);
|
||||
|
||||
// TODO: We currently do not support keystore passwords
|
||||
|
@ -234,7 +235,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
}
|
||||
|
||||
private void setParser() {
|
||||
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request.").withOptionalArg();
|
||||
urlOption = parser.acceptsAll(Arrays.asList("u", "url"), "The url for the change password request.").withRequiredArg();
|
||||
noPromptOption = parser.acceptsAll(Arrays.asList("b", "batch"),
|
||||
"If enabled, run the change password process without prompting the user.").withOptionalArg();
|
||||
}
|
||||
|
@ -257,14 +258,12 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
* where to write verbose info.
|
||||
*/
|
||||
void checkElasticKeystorePasswordValid(Terminal terminal, Environment env) throws Exception {
|
||||
URL route = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||
URL route = new URL(url, (url.getPath() + "/_xpack/security/_authenticate").replaceAll("/+", "/") + "?pretty");
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, "Testing if bootstrap password is valid for " + route.toString());
|
||||
try {
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, "Testing if bootstrap password is valid for " + route.toString());
|
||||
int httpCode = client.postURL("GET", route, elasticUser, elasticUserPassword, () -> null, is -> {
|
||||
byte[] bytes = Streams.readAll(is);
|
||||
terminal.println(Verbosity.VERBOSE, new String(bytes, StandardCharsets.UTF_8));
|
||||
});
|
||||
final int httpCode = client.postURL("GET", route, elasticUser, elasticUserPassword, () -> null,
|
||||
is -> verboseLogResponse(is, terminal));
|
||||
// keystore password is not valid
|
||||
if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
terminal.println("");
|
||||
|
@ -275,23 +274,33 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
terminal.println(" This tool used the keystore at " + KeyStoreWrapper.keystorePath(env.configFile()));
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.CONFIG, "Failed to verify bootstrap password");
|
||||
} else if (httpCode != HttpURLConnection.HTTP_OK) {
|
||||
terminal.println("");
|
||||
terminal.println("Unexpected response code [" + httpCode + "] from calling GET " + route.toString());
|
||||
terminal.println("Possible causes include:");
|
||||
terminal.println(" * The relative path of the URL is incorrect. Is there a proxy in-between?");
|
||||
terminal.println(" * The protocol (http/https) does not match the port.");
|
||||
terminal.println(" * Is this really an Elasticsearch server?");
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.CONFIG, "Uknown error");
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
terminal.println("");
|
||||
terminal.println("Cannot connect to elasticsearch node.");
|
||||
e.printStackTrace(terminal.getWriter());
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.CONFIG,
|
||||
"Failed to connect to elasticsearch at " + route.toString() + ". Is the URL correct and elasticsearch running?", e);
|
||||
} catch (SSLException e) {
|
||||
terminal.println("");
|
||||
terminal.println("SSL connection to " + route.toString() + " failed: " + e.getMessage());
|
||||
terminal.println("Please check the elasticsearch SSL settings under " + CommandLineHttpClient.HTTP_SSL_SETTING);
|
||||
terminal.println("");
|
||||
e.printStackTrace(terminal.getWriter());
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.CONFIG,
|
||||
"Failed to establish SSL connection to elasticsearch at " + route.toString() + ". ", e);
|
||||
} catch (IOException e) {
|
||||
terminal.println("");
|
||||
terminal.println("Connection failure to: " + route.toString() + " failed: " + e.getMessage());
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.CONFIG, "Failed to connect to elasticsearch at " +
|
||||
route.toString() + ". Is the URL correct and elasticsearch running?", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,12 +312,14 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
* @param password
|
||||
* the new password of the user.
|
||||
*/
|
||||
private void changeUserPassword(String user, SecureString password) throws Exception {
|
||||
URL route = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||
private void changeUserPassword(String user, SecureString password, Terminal terminal) throws Exception {
|
||||
URL route = new URL(url, (url.getPath() + "/_xpack/security/user/" + user + "/_password").replaceAll("/+", "/") + "?pretty");
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, "Trying user password change call " + route.toString());
|
||||
try {
|
||||
// supplier should own his resources
|
||||
SecureString supplierPassword = password.clone();
|
||||
client.postURL("PUT", route, elasticUser, elasticUserPassword, () -> {
|
||||
final int httpCode = client.postURL("PUT", route, elasticUser, elasticUserPassword, () -> {
|
||||
try {
|
||||
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
|
||||
xContentBuilder.startObject().field("password", supplierPassword.toString()).endObject();
|
||||
|
@ -316,9 +327,24 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
} finally {
|
||||
supplierPassword.close();
|
||||
}
|
||||
}, is -> {
|
||||
});
|
||||
}, is -> verboseLogResponse(is, terminal));
|
||||
if (httpCode != HttpURLConnection.HTTP_OK) {
|
||||
terminal.println("");
|
||||
terminal.println("Unexpected response code [" + httpCode + "] from calling PUT " + route.toString());
|
||||
terminal.println("Possible next steps:");
|
||||
terminal.println("* Try running this tool again.");
|
||||
terminal.println("* Check the elasticsearch logs for additional error details.");
|
||||
terminal.println("* Use the change password API manually. ");
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.TEMP_FAILURE,
|
||||
"Failed to set password for user [" + user + "].");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
terminal.println("");
|
||||
terminal.println("Connection failure to: " + route.toString() + " failed: " + e.getMessage());
|
||||
terminal.println(Verbosity.VERBOSE, "");
|
||||
terminal.println(Verbosity.VERBOSE, ExceptionsHelper.stackTrace(e));
|
||||
terminal.println("");
|
||||
throw new UserException(ExitCodes.TEMP_FAILURE, "Failed to set password for user [" + user + "].", e);
|
||||
}
|
||||
}
|
||||
|
@ -333,7 +359,7 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
* Callback for each successful operation
|
||||
*/
|
||||
void changePasswords(CheckedFunction<String, SecureString, UserException> passwordFn,
|
||||
CheckedBiConsumer<String, SecureString, Exception> successCallback) throws Exception {
|
||||
CheckedBiConsumer<String, SecureString, Exception> successCallback, Terminal terminal) throws Exception {
|
||||
Map<String, SecureString> passwordsMap = new HashMap<>(USERS.size());
|
||||
try {
|
||||
for (String user : USERS) {
|
||||
|
@ -350,17 +376,27 @@ public class SetupPasswordTool extends MultiCommand {
|
|||
superUserEntry = entry;
|
||||
continue;
|
||||
}
|
||||
changeUserPassword(entry.getKey(), entry.getValue());
|
||||
changeUserPassword(entry.getKey(), entry.getValue(), terminal);
|
||||
successCallback.accept(entry.getKey(), entry.getValue());
|
||||
}
|
||||
// change elastic superuser
|
||||
if (superUserEntry != null) {
|
||||
changeUserPassword(superUserEntry.getKey(), superUserEntry.getValue());
|
||||
changeUserPassword(superUserEntry.getKey(), superUserEntry.getValue(), terminal);
|
||||
successCallback.accept(superUserEntry.getKey(), superUserEntry.getValue());
|
||||
}
|
||||
} finally {
|
||||
passwordsMap.forEach((user, pass) -> pass.close());
|
||||
}
|
||||
}
|
||||
|
||||
private void verboseLogResponse(InputStream is, Terminal terminal) throws IOException {
|
||||
if (is != null) {
|
||||
byte[] bytes = Streams.readAll(is);
|
||||
terminal.println(Verbosity.VERBOSE, new String(bytes, StandardCharsets.UTF_8));
|
||||
} else {
|
||||
terminal.println(Verbosity.VERBOSE, "<Empty response>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,9 +28,9 @@ import org.junit.Before;
|
|||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -38,7 +38,7 @@ import java.util.Collections;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.any;
|
||||
|
@ -124,26 +124,31 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
|||
}
|
||||
|
||||
public void testAutoSetup() throws Exception {
|
||||
String url = httpClient.getDefaultURL();
|
||||
execute("auto", pathHomeParameter, "-b", "true");
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
if (randomBoolean()) {
|
||||
execute("auto", pathHomeParameter, "-b", "true");
|
||||
} else {
|
||||
terminal.addTextInput("Y");
|
||||
execute("auto", pathHomeParameter);
|
||||
}
|
||||
|
||||
verify(keyStore).decrypt(new char[0]);
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||
|
||||
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||
URL checkUrl = checkURL(url);
|
||||
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class));
|
||||
for (String user : usersInSetOrder) {
|
||||
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||
URL urlWithRoute = passwdURL(url, user);
|
||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||
any(CheckedSupplier.class), any(CheckedConsumer.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void testAuthnFail() throws Exception {
|
||||
URL authnURL = new URL(httpClient.getDefaultURL() + "/_xpack/security/_authenticate?pretty");
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
URL authnURL = checkURL(url);
|
||||
when(httpClient.postURL(eq("GET"), eq(authnURL), eq(ElasticUser.NAME), any(SecureString.class), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class))).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED);
|
||||
|
||||
|
@ -153,38 +158,66 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
|||
} catch (UserException e) {
|
||||
assertEquals(ExitCodes.CONFIG, e.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public void testWrongServer() throws Exception {
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
URL authnURL = checkURL(url);
|
||||
when(httpClient.postURL(eq("GET"), eq(authnURL), eq(ElasticUser.NAME), any(SecureString.class), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class))).thenThrow(randomFrom(IOException.class, SSLException.class));
|
||||
|
||||
try {
|
||||
execute(randomBoolean() ? "auto" : "interactive", pathHomeParameter);
|
||||
fail("Should have thrown exception");
|
||||
} catch (UserException e) {
|
||||
assertEquals(ExitCodes.CONFIG, e.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public void testUrlOption() throws Exception {
|
||||
String url = "http://localhost:9202";
|
||||
execute("auto", pathHomeParameter, "-u", url, "-b");
|
||||
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 = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||
URL checkUrl = checkURL(url);
|
||||
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class));
|
||||
for (String user : usersInSetOrder) {
|
||||
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||
URL urlWithRoute = passwdURL(url, user);
|
||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||
any(CheckedSupplier.class), any(CheckedConsumer.class));
|
||||
}
|
||||
}
|
||||
|
||||
public void testSetUserPassFail() throws Exception {
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
String userToFail = randomFrom(SetupPasswordTool.USERS);
|
||||
URL userToFailURL = passwdURL(url, userToFail);
|
||||
|
||||
when(httpClient.postURL(eq("PUT"), eq(userToFailURL), anyString(), any(SecureString.class), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class))).thenThrow(IOException.class);
|
||||
try {
|
||||
execute(randomBoolean() ? "auto" : "interactive", pathHomeParameter, "-b");
|
||||
fail("Should have thrown exception");
|
||||
} catch (UserException e) {
|
||||
assertEquals(ExitCodes.TEMP_FAILURE, e.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public void testInteractiveSetup() throws Exception {
|
||||
String url = httpClient.getDefaultURL();
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
|
||||
terminal.addTextInput("Y");
|
||||
execute("interactive", pathHomeParameter);
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||
|
||||
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||
URL checkUrl = checkURL(url);
|
||||
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class));
|
||||
for (String user : usersInSetOrder) {
|
||||
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||
URL urlWithRoute = passwdURL(url, user);
|
||||
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
|
||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||
passwordCaptor.capture(), any(CheckedConsumer.class));
|
||||
|
@ -193,7 +226,7 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
|||
}
|
||||
|
||||
public void testInteractivePasswordsFatFingers() throws Exception {
|
||||
String url = httpClient.getDefaultURL();
|
||||
URL url = new URL(httpClient.getDefaultURL());
|
||||
|
||||
terminal.reset();
|
||||
terminal.addTextInput("Y");
|
||||
|
@ -218,11 +251,11 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
|||
|
||||
InOrder inOrder = Mockito.inOrder(httpClient);
|
||||
|
||||
URL checkUrl = new URL(url + "/_xpack/security/_authenticate?pretty");
|
||||
URL checkUrl = checkURL(url);
|
||||
inOrder.verify(httpClient).postURL(eq("GET"), eq(checkUrl), eq(ElasticUser.NAME), eq(bootstrapPassword), any(CheckedSupplier.class),
|
||||
any(CheckedConsumer.class));
|
||||
for (String user : usersInSetOrder) {
|
||||
URL urlWithRoute = new URL(url + "/_xpack/security/user/" + user + "/_password");
|
||||
URL urlWithRoute = passwdURL(url, user);
|
||||
ArgumentCaptor<CheckedSupplier<String, Exception>> passwordCaptor = ArgumentCaptor.forClass((Class) CheckedSupplier.class);
|
||||
inOrder.verify(httpClient).postURL(eq("PUT"), eq(urlWithRoute), eq(ElasticUser.NAME), eq(bootstrapPassword),
|
||||
passwordCaptor.capture(), any(CheckedConsumer.class));
|
||||
|
@ -243,4 +276,12 @@ public class SetupPasswordToolTests extends CommandTestCase {
|
|||
}
|
||||
throw new RuntimeException("Did not properly parse password.");
|
||||
}
|
||||
|
||||
private URL checkURL(URL url) throws MalformedURLException {
|
||||
return new URL(url, (url.getPath() + "/_xpack/security/_authenticate").replaceAll("/+", "/") + "?pretty");
|
||||
}
|
||||
|
||||
private URL passwdURL(URL url, String user) throws MalformedURLException {
|
||||
return new URL(url, (url.getPath() + "/_xpack/security/user/" + user + "/_password").replaceAll("/+", "/") + "?pretty");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ internal:cluster/nodes/indices/shard/store[n]
|
|||
internal:cluster/shard/failure
|
||||
internal:cluster/shard/started
|
||||
internal:cluster/snapshot/update_snapshot
|
||||
internal:cluster/snapshot/update_snapshot_status
|
||||
internal:discovery/zen/fd/master_ping
|
||||
internal:discovery/zen/fd/ping
|
||||
internal:discovery/zen/join
|
||||
|
|
Loading…
Reference in New Issue