blob: 52566f6360af28a86df2bf91e9916202b28ae09f [file] [log] [blame]
/*
* 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.ignite.internal.commandline;
import java.io.File;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.UUID;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;
import java.util.stream.Collectors;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.client.GridClientAuthenticationException;
import org.apache.ignite.internal.client.GridClientClosedException;
import org.apache.ignite.internal.client.GridClientConfiguration;
import org.apache.ignite.internal.client.GridClientDisconnectedException;
import org.apache.ignite.internal.client.GridClientHandshakeException;
import org.apache.ignite.internal.client.GridServerUnreachableException;
import org.apache.ignite.internal.client.impl.connection.GridClientConnectionResetException;
import org.apache.ignite.internal.client.ssl.GridSslBasicContextFactory;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.logger.java.JavaLoggerFileHandler;
import org.apache.ignite.logger.java.JavaLoggerFormatter;
import org.apache.ignite.plugin.security.SecurityCredentials;
import org.apache.ignite.plugin.security.SecurityCredentialsBasicProvider;
import org.apache.ignite.plugin.security.SecurityCredentialsProvider;
import org.apache.ignite.ssl.SslContextFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static java.lang.System.lineSeparator;
import static java.util.Objects.nonNull;
import static org.apache.ignite.internal.IgniteVersionUtils.ACK_VER_STR;
import static org.apache.ignite.internal.IgniteVersionUtils.COPYRIGHT;
import static org.apache.ignite.internal.commandline.CommandLogger.DOUBLE_INDENT;
import static org.apache.ignite.internal.commandline.CommandLogger.INDENT;
import static org.apache.ignite.internal.commandline.CommandLogger.errorMessage;
import static org.apache.ignite.internal.commandline.CommandLogger.optional;
import static org.apache.ignite.internal.commandline.CommonArgParser.CMD_AUTO_CONFIRMATION;
import static org.apache.ignite.internal.commandline.CommonArgParser.CMD_VERBOSE;
import static org.apache.ignite.internal.commandline.CommonArgParser.getCommonOptions;
import static org.apache.ignite.internal.commandline.TaskExecutor.DFLT_HOST;
import static org.apache.ignite.internal.commandline.TaskExecutor.DFLT_PORT;
import static org.apache.ignite.ssl.SslContextFactory.DFLT_SSL_PROTOCOL;
/**
* Class that execute several commands passed via command line.
*/
public class CommandHandler {
/** */
static final String CMD_HELP = "--help";
/** */
public static final String CONFIRM_MSG = "y";
/** */
static final String DELIM = "--------------------------------------------------------------------------------";
/** */
public static final int EXIT_CODE_OK = 0;
/** */
public static final int EXIT_CODE_INVALID_ARGUMENTS = 1;
/** */
public static final int EXIT_CODE_CONNECTION_FAILED = 2;
/** */
public static final int ERR_AUTHENTICATION_FAILED = 3;
/** */
public static final int EXIT_CODE_UNEXPECTED_ERROR = 4;
/** */
private static final long DFLT_PING_INTERVAL = 5000L;
/** */
private static final long DFLT_PING_TIMEOUT = 30_000L;
/** */
private final Scanner in = new Scanner(System.in);
/** Utility name. */
public static final String UTILITY_NAME = "control.(sh|bat)";
/** */
public static final String NULL = "null";
/** JULs logger. */
private final Logger logger;
/** Session. */
protected final String ses = U.id8(UUID.randomUUID());
/** Console instance. Public access needs for tests. */
public GridConsole console = GridConsoleAdapter.getInstance();
/** */
private Object lastOperationRes;
/**
* @param args Arguments to parse and apply.
*/
public static void main(String[] args) {
CommandHandler hnd = new CommandHandler();
System.exit(hnd.execute(Arrays.asList(args)));
}
/**
* @return prepared JULs logger.
*/
private Logger setupJavaLogger() {
Logger result = initLogger(CommandHandler.class.getName() + "Log");
// Adding logging to file.
try {
String absPathPattern = new File(JavaLoggerFileHandler.logDirectory(U.defaultWorkDirectory()), "control-utility-%g.log").getAbsolutePath();
FileHandler fileHandler = new FileHandler(absPathPattern, 5 * 1024 * 1024, 5);
fileHandler.setFormatter(new JavaLoggerFormatter());
result.addHandler(fileHandler);
}
catch (Exception e) {
System.out.println("Failed to configure logging to file");
}
// Adding logging to console.
result.addHandler(setupStreamHandler());
return result;
}
/**
* @return StreamHandler with empty formatting
*/
public static StreamHandler setupStreamHandler() {
return new StreamHandler(System.out, new Formatter() {
@Override public String format(LogRecord record) {
return record.getMessage() + "\n";
}
});
}
/**
* Initialises JULs logger with basic settings
* @param loggerName logger name. If {@code null} anonymous logger is returned.
* @return logger
*/
public static Logger initLogger(@Nullable String loggerName) {
Logger result;
if (loggerName == null)
result = Logger.getAnonymousLogger();
else
result = Logger.getLogger(loggerName);
result.setLevel(Level.INFO);
result.setUseParentHandlers(false);
return result;
}
/**
*
*/
public CommandHandler() {
logger = setupJavaLogger();
}
/**
* @param logger Logger to use.
*/
public CommandHandler(Logger logger) {
this.logger = logger;
}
/**
* Parse and execute command.
*
* @param rawArgs Arguments to parse and execute.
* @return Exit code.
*/
public int execute(List<String> rawArgs) {
LocalDateTime startTime = LocalDateTime.now();
Thread.currentThread().setName("session=" + ses);
logger.info("Control utility [ver. " + ACK_VER_STR + "]");
logger.info(COPYRIGHT);
logger.info("User: " + System.getProperty("user.name"));
logger.info("Time: " + startTime);
String commandName = "";
Throwable err = null;
boolean verbose = false;
try {
if (F.isEmpty(rawArgs) || (rawArgs.size() == 1 && CMD_HELP.equalsIgnoreCase(rawArgs.get(0)))) {
printHelp();
return EXIT_CODE_OK;
}
verbose = F.exist(rawArgs, CMD_VERBOSE::equalsIgnoreCase);
ConnectionAndSslParameters args = new CommonArgParser(logger).parseAndValidate(rawArgs.iterator());
Command command = args.command();
commandName = command.name();
GridClientConfiguration clientCfg = getClientConfiguration(args);
int tryConnectMaxCount = 3;
boolean suppliedAuth = !F.isEmpty(args.userName()) && !F.isEmpty(args.password());
boolean credentialsRequested = false;
while (true) {
try {
if (!args.autoConfirmation()) {
command.prepareConfirmation(clientCfg);
if (!confirm(command.confirmationPrompt())) {
logger.info("Operation cancelled.");
return EXIT_CODE_OK;
}
}
logger.info("Command [" + commandName + "] started");
logger.info("Arguments: " + String.join(" ", rawArgs));
logger.info(DELIM);
lastOperationRes = command.execute(clientCfg, logger);
break;
}
catch (Throwable e) {
if (!isAuthError(e))
throw e;
if (suppliedAuth)
throw new GridClientAuthenticationException("Wrong credentials.");
if (tryConnectMaxCount == 0) {
throw new GridClientAuthenticationException("Maximum number of " +
"retries exceeded");
}
logger.info(credentialsRequested ?
"Authentication error, please try again." :
"This cluster requires authentication.");
if (credentialsRequested)
tryConnectMaxCount--;
String user = retrieveUserName(args, clientCfg);
String pwd = new String(requestPasswordFromConsole("password: "));
clientCfg = getClientConfiguration(user, pwd, args);
credentialsRequested = true;
}
}
logger.info("Command [" + commandName + "] finished with code: " + EXIT_CODE_OK);
return EXIT_CODE_OK;
}
catch (IllegalArgumentException e) {
logger.severe("Check arguments. " + errorMessage(e));
logger.info("Command [" + commandName + "] finished with code: " + EXIT_CODE_INVALID_ARGUMENTS);
if (verbose)
err = e;
return EXIT_CODE_INVALID_ARGUMENTS;
}
catch (Throwable e) {
if (isAuthError(e)) {
logger.severe("Authentication error. " + errorMessage(e));
logger.info("Command [" + commandName + "] finished with code: " + ERR_AUTHENTICATION_FAILED);
if (verbose)
err = e;
return ERR_AUTHENTICATION_FAILED;
}
if (isConnectionError(e)) {
IgniteCheckedException cause = X.cause(e, IgniteCheckedException.class);
if (isConnectionClosedSilentlyException(e))
logger.severe("Connection to cluster failed. Please check firewall settings and " +
"client and server are using the same SSL configuration.");
else {
if (isSSLMisconfigurationError(cause))
e = cause;
logger.severe("Connection to cluster failed. " + errorMessage(e));
}
logger.info("Command [" + commandName + "] finished with code: " + EXIT_CODE_CONNECTION_FAILED);
if (verbose)
err = e;
return EXIT_CODE_CONNECTION_FAILED;
}
if (X.hasCause(e, IllegalArgumentException.class)) {
IllegalArgumentException iae = X.cause(e, IllegalArgumentException.class);
logger.severe("Check arguments. " + errorMessage(iae));
logger.info("Command [" + commandName + "] finished with code: " + EXIT_CODE_INVALID_ARGUMENTS);
if (verbose)
err = e;
return EXIT_CODE_INVALID_ARGUMENTS;
}
logger.severe(errorMessage(e));
logger.info("Command [" + commandName + "] finished with code: " + EXIT_CODE_UNEXPECTED_ERROR);
err = e;
return EXIT_CODE_UNEXPECTED_ERROR;
}
finally {
LocalDateTime endTime = LocalDateTime.now();
Duration diff = Duration.between(startTime, endTime);
if (nonNull(err))
logger.info("Error stack trace:" + System.lineSeparator() + X.getFullStackTrace(err));
logger.info("Control utility has completed execution at: " + endTime);
logger.info("Execution time: " + diff.toMillis() + " ms");
Arrays.stream(logger.getHandlers())
.filter(handler -> handler instanceof FileHandler)
.forEach(Handler::close);
}
}
/**
* Analyses passed exception to find out whether it is related to SSL misconfiguration issues.
*
* (!) Implementation depends heavily on structure of exception stack trace
* thus is very fragile to any changes in that structure.
*
* @param e Exception to analyze.
*
* @return {@code True} if exception may be related to SSL misconfiguration issues.
*/
private boolean isSSLMisconfigurationError(Throwable e) {
return e != null && e.getMessage() != null && e.getMessage().contains("SSL");
}
/**
* Analyses passed exception to find out whether it is caused by server closing connection silently.
* This happens when client tries to establish unprotected connection
* to the cluster supporting only secured communications (e.g. when server is configured to use SSL certificates
* and client is not).
*
* (!) Implementation depends heavily on structure of exception stack trace
* thus is very fragile to any changes in that structure.
*
* @param e Exception to analyse.
* @return {@code True} if exception may be related to the attempt to establish unprotected connection
* to secured cluster.
*/
private boolean isConnectionClosedSilentlyException(Throwable e) {
if (!(e instanceof GridClientDisconnectedException))
return false;
Throwable cause = e.getCause();
if (cause == null)
return false;
cause = cause.getCause();
if (cause instanceof GridClientConnectionResetException &&
cause.getMessage() != null &&
cause.getMessage().contains("Failed to perform handshake")
)
return true;
return false;
}
/**
* Does one of three things:
* <ul>
* <li>returns user name from connection parameters if it is there;</li>
* <li>returns user name from client configuration if it is there;</li>
* <li>requests user input and returns entered name.</li>
* </ul>
*
* @param args Connection parameters.
* @param clientCfg Client configuration.
* @throws IgniteCheckedException If security credetials cannot be provided from client configuration.
*/
private String retrieveUserName(
ConnectionAndSslParameters args,
GridClientConfiguration clientCfg
) throws IgniteCheckedException {
if (!F.isEmpty(args.userName()))
return args.userName();
else if (clientCfg.getSecurityCredentialsProvider() == null)
return requestDataFromConsole("user: ");
else
return (String)clientCfg.getSecurityCredentialsProvider().credentials().getLogin();
}
/**
* @param args Common arguments.
* @return Thin client configuration to connect to cluster.
* @throws IgniteCheckedException If error occur.
*/
@NotNull private GridClientConfiguration getClientConfiguration(
ConnectionAndSslParameters args
) throws IgniteCheckedException {
return getClientConfiguration(args.userName(), args.password(), args);
}
/**
* @param userName User name for authorization.
* @param password Password for authorization.
* @param args Common arguments.
* @return Thin client configuration to connect to cluster.
* @throws IgniteCheckedException If error occur.
*/
@NotNull private GridClientConfiguration getClientConfiguration(
String userName,
String password,
ConnectionAndSslParameters args
) throws IgniteCheckedException {
GridClientConfiguration clientCfg = new GridClientConfiguration();
clientCfg.setPingInterval(args.pingInterval());
clientCfg.setPingTimeout(args.pingTimeout());
clientCfg.setServers(Collections.singletonList(args.host() + ":" + args.port()));
if (!F.isEmpty(userName))
clientCfg.setSecurityCredentialsProvider(getSecurityCredentialsProvider(userName, password, clientCfg));
if (!F.isEmpty(args.sslKeyStorePath()))
clientCfg.setSslContextFactory(createSslSupportFactory(args));
return clientCfg;
}
/**
* @param userName User name for authorization.
* @param password Password for authorization.
* @param clientCfg Thin client configuration to connect to cluster.
* @return Security credentials provider with usage of given user name and password.
* @throws IgniteCheckedException If error occur.
*/
@NotNull private SecurityCredentialsProvider getSecurityCredentialsProvider(
String userName,
String password,
GridClientConfiguration clientCfg
) throws IgniteCheckedException {
SecurityCredentialsProvider securityCredential = clientCfg.getSecurityCredentialsProvider();
if (securityCredential == null)
return new SecurityCredentialsBasicProvider(new SecurityCredentials(userName, password));
final SecurityCredentials credential = securityCredential.credentials();
credential.setLogin(userName);
credential.setPassword(password);
return securityCredential;
}
/**
* @param args Commond args.
* @return Ssl support factory.
*/
@NotNull private GridSslBasicContextFactory createSslSupportFactory(ConnectionAndSslParameters args) {
GridSslBasicContextFactory factory = new GridSslBasicContextFactory();
List<String> sslProtocols = split(args.sslProtocol(), ",");
String sslProtocol = F.isEmpty(sslProtocols) ? DFLT_SSL_PROTOCOL : sslProtocols.get(0);
factory.setProtocol(sslProtocol);
factory.setKeyAlgorithm(args.sslKeyAlgorithm());
if (sslProtocols.size() > 1)
factory.setProtocols(sslProtocols);
factory.setCipherSuites(split(args.getSslCipherSuites(), ","));
factory.setKeyStoreFilePath(args.sslKeyStorePath());
if (args.sslKeyStorePassword() != null)
factory.setKeyStorePassword(args.sslKeyStorePassword());
else {
char[] keyStorePwd = requestPasswordFromConsole("SSL keystore password: ");
args.sslKeyStorePassword(keyStorePwd);
factory.setKeyStorePassword(keyStorePwd);
}
factory.setKeyStoreType(args.sslKeyStoreType());
if (F.isEmpty(args.sslTrustStorePath()))
factory.setTrustManagers(GridSslBasicContextFactory.getDisabledTrustManager());
else {
factory.setTrustStoreFilePath(args.sslTrustStorePath());
if (args.sslTrustStorePassword() != null)
factory.setTrustStorePassword(args.sslTrustStorePassword());
else {
char[] trustStorePwd = requestPasswordFromConsole("SSL truststore password: ");
args.sslTrustStorePassword(trustStorePwd);
factory.setTrustStorePassword(trustStorePwd);
}
factory.setTrustStoreType(args.sslTrustStoreType());
}
return factory;
}
/**
* Used for tests.
*
* @return Last operation result;
*/
public <T> T getLastOperationResult() {
return (T)lastOperationRes;
}
/**
* Provides a prompt, then reads a single line of text from the console.
*
* @param prompt text
* @return A string containing the line read from the console
*/
private String readLine(String prompt) {
System.out.print(prompt);
return in.nextLine();
}
/**
* Requests interactive user confirmation if forthcoming operation is dangerous.
*
* @return {@code true} if operation confirmed (or not needed), {@code false} otherwise.
*/
private boolean confirm(String str) {
if (str == null)
return true;
String prompt = str + lineSeparator() + "Press '" + CONFIRM_MSG + "' to continue . . . ";
return CONFIRM_MSG.equalsIgnoreCase(readLine(prompt));
}
/**
* @param e Exception to check.
* @return {@code true} if specified exception is {@link GridClientAuthenticationException}.
*/
public static boolean isAuthError(Throwable e) {
return X.hasCause(e, GridClientAuthenticationException.class);
}
/**
* @param e Exception to check.
* @return {@code true} if specified exception is a connection error.
*/
private static boolean isConnectionError(Throwable e) {
return e instanceof GridClientClosedException ||
e instanceof GridClientConnectionResetException ||
e instanceof GridClientDisconnectedException ||
e instanceof GridClientHandshakeException ||
e instanceof GridServerUnreachableException;
}
/**
* Requests password from console with message.
*
* @param msg Message.
* @return Password.
*/
private char[] requestPasswordFromConsole(String msg) {
if (console == null)
throw new UnsupportedOperationException("Failed to securely read password (console is unavailable): " + msg);
else
return console.readPassword(msg);
}
/**
* Requests user data from console with message.
*
* @param msg Message.
* @return Input user data.
*/
private String requestDataFromConsole(String msg) {
if (console != null)
return console.readLine(msg);
else {
Scanner scanner = new Scanner(System.in);
logger.info(msg);
return scanner.nextLine();
}
}
/**
* Split string into items.
*
* @param s String to process.
* @param delim Delimiter.
* @return List with items.
*/
private static List<String> split(String s, String delim) {
if (F.isEmpty(s))
return Collections.emptyList();
return Arrays.stream(s.split(delim))
.map(String::trim)
.filter(item -> !item.isEmpty())
.collect(Collectors.toList());
}
/** */
private void printHelp() {
logger.info("Control utility script is used to execute admin commands on cluster or get common cluster info. " +
"The command has the following syntax:");
logger.info("");
logger.info(INDENT + CommandLogger.join(" ", CommandLogger.join(" ", UTILITY_NAME, CommandLogger.join(" ", getCommonOptions())),
optional("command"), "<command_parameters>"));
logger.info("");
logger.info("");
logger.info("This utility can do the following commands:");
Arrays.stream(CommandList.values()).forEach(c -> c.command().printUsage(logger));
logger.info("");
logger.info("By default commands affecting the cluster require interactive confirmation.");
logger.info("Use " + CMD_AUTO_CONFIRMATION + " option to disable it.");
logger.info("");
logger.info("Default values:");
logger.info(DOUBLE_INDENT + "HOST_OR_IP=" + DFLT_HOST);
logger.info(DOUBLE_INDENT + "PORT=" + DFLT_PORT);
logger.info(DOUBLE_INDENT + "PING_INTERVAL=" + DFLT_PING_INTERVAL);
logger.info(DOUBLE_INDENT + "PING_TIMEOUT=" + DFLT_PING_TIMEOUT);
logger.info(DOUBLE_INDENT + "SSL_PROTOCOL=" + SslContextFactory.DFLT_SSL_PROTOCOL);
logger.info(DOUBLE_INDENT + "SSL_KEY_ALGORITHM=" + SslContextFactory.DFLT_KEY_ALGORITHM);
logger.info(DOUBLE_INDENT + "KEYSTORE_TYPE=" + SslContextFactory.DFLT_STORE_TYPE);
logger.info(DOUBLE_INDENT + "TRUSTSTORE_TYPE=" + SslContextFactory.DFLT_STORE_TYPE);
logger.info("");
logger.info("Exit codes:");
logger.info(DOUBLE_INDENT + EXIT_CODE_OK + " - successful execution.");
logger.info(DOUBLE_INDENT + EXIT_CODE_INVALID_ARGUMENTS + " - invalid arguments.");
logger.info(DOUBLE_INDENT + EXIT_CODE_CONNECTION_FAILED + " - connection failed.");
logger.info(DOUBLE_INDENT + ERR_AUTHENTICATION_FAILED + " - authentication failed.");
logger.info(DOUBLE_INDENT + EXIT_CODE_UNEXPECTED_ERROR + " - unexpected error.");
}
}