blob: 96dd62d12decf5bbe236f2882bc0fe20813b6362 [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.geode.admin.internal;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_CIPHERS;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_ENABLED;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_PROTOCOLS;
import static org.apache.geode.distributed.ConfigurationProperties.CLUSTER_SSL_REQUIRE_AUTHENTICATION;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.internal.net.InetAddressUtils.isLocalHost;
import java.io.File;
import java.util.Iterator;
import java.util.Properties;
import org.apache.logging.log4j.Logger;
import org.apache.geode.admin.AdminDistributedSystem;
import org.apache.geode.admin.DistributedSystemConfig;
import org.apache.geode.admin.ManagedEntity;
import org.apache.geode.admin.ManagedEntityConfig;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.internal.ProcessOutputReader;
import org.apache.geode.logging.internal.executors.LoggingThread;
import org.apache.geode.logging.internal.log4j.api.LogService;
/**
* Implements the actual administration (starting, stopping, etc.) of GemFire
* {@link ManagedEntity}s. It {@link Runtime#exec(java.lang.String) executes} commands to administer
* the entities based on information provided by the {@link InternalManagedEntity} object. Note that
* it does not use <code>SystemAdmin</code> to manage "local" entities; it always execs the scripts.
*
* <P>
*
* This class is a refactoring of <code>Systemcontroller</code>, <code>RemoteCommand</code>, and
* <code>LocatorRemoteCommand</code>.
*
* @since GemFire 4.0
*/
class EnabledManagedEntityController implements ManagedEntityController {
private static final Logger logger = LogService.getLogger();
/** Known strings found in output indicating error. */
private static final String[] ERROR_OUTPUTS = new String[] {"No such file or directory",
"The system cannot find the file specified.", "Access is denied.", "cannot open", "ERROR"};
/** Token in command prefix to be replaced with actual HOST */
private static final String HOST = "{HOST}";
/** Token in command prefix to be replaced with actual execution CMD */
private static final String CMD = "{CMD}";
////////////////////// Instance Fields //////////////////////
/** System to which the managed entities belong */
private final AdminDistributedSystem system;
/////////////////////// Constructors ///////////////////////
/**
* Creates a new <code>ManagedEntityController</code> for entities in the given distributed
* system.
*/
EnabledManagedEntityController(AdminDistributedSystem system) {
this.system = system;
}
///////////////////// Instance Methods /////////////////////
/**
* Returns <code>true</code> if the <code>output</code> string contains a known error message.
*/
private boolean outputIsError(String output) {
if (output == null)
return false;
boolean error = false;
for (int i = 0; i < ERROR_OUTPUTS.length; i++) {
error = output.indexOf(ERROR_OUTPUTS[i]) > -1;
if (error)
return error;
}
return error;
}
/**
* Executes a command using {@link Runtime#exec(java.lang.String)}.
*
* @param command The full command to remotely execute
*
* @return Output from the command that was executed or <code>null</code> if the executing the
* command failed.
*/
protected String execute(String command, InternalManagedEntity entity) {
/*
* TODO: this is getting ugly... clients of this method really need to have the ability to do
* their own parsing/checking of 'output'
*/
if (command == null || command.length() == 0) {
throw new IllegalArgumentException(
"Execution command is empty");
}
File workingDir = new File(entity.getEntityConfig().getWorkingDirectory());
logger.info("Executing remote command: {} in directory {}",
command, workingDir);
Process p = null;
try {
p = Runtime.getRuntime().exec(command, null /* env */, workingDir);
} catch (java.io.IOException e) {
logger.fatal("While executing " + command, e);
return null;
}
final ProcessOutputReader pos = new ProcessOutputReader(p);
int retCode = pos.getExitCode();
final String output = pos.getOutput();
logger.info("Result of executing {} is {}", command, Integer.valueOf(retCode));
logger.info("Output of {} is {}", command, output);
if (retCode != 0 || outputIsError(output)) {
logger.warn("Remote execution of {} failed.", command);
return null;
}
return output;
}
/** Returns true if the path ends with a path separator. */
private boolean endsWithSeparator(String path) {
return path.endsWith("/") || path.endsWith("\\");
}
/** Translates the path between Windows and UNIX. */
private String getOSPath(String path) {
if (pathIsWindows(path)) {
return path.replace('/', '\\');
} else {
return path.replace('\\', '/');
}
}
/** Returns true if the path is on Windows. */
private boolean pathIsWindows(String path) {
if (path != null && path.length() > 1) {
return (Character.isLetter(path.charAt(0)) && path.charAt(1) == ':')
|| (path.startsWith("//") || path.startsWith("\\\\"));
}
return false;
}
/**
* If the managed entity resides on a remote host, then <code>command</code> is munged to take the
* remote command into account.
*
* @throws IllegalStateException If a remote command is required, but one has not been specified.
*/
private String arrangeRemoteCommand(InternalManagedEntity entity, String cmd) {
String host = entity.getEntityConfig().getHost();
if (isLocalHost(host)) {
// No arranging necessary
return cmd;
}
String prefix = entity.getEntityConfig().getRemoteCommand();
if (prefix == null || prefix.length() <= 0) {
prefix = entity.getDistributedSystem().getRemoteCommand();
}
if (prefix == null || prefix.length() <= 0) {
throw new IllegalStateException(
String.format(
"A remote command must be specified to operate on a managed entity on host %s",
host));
}
int hostIdx = prefix.indexOf(HOST);
int cmdIdx = prefix.indexOf(CMD);
if (hostIdx == -1 && cmdIdx == -1) {
return prefix + " " + host + " " + cmd;
}
if (hostIdx >= 0) {
String start = prefix.substring(0, hostIdx);
String end = null;
if (hostIdx + HOST.length() >= prefix.length()) {
end = "";
} else {
end = prefix.substring(hostIdx + HOST.length());
}
prefix = start + host + end;
cmdIdx = prefix.indexOf(CMD); // recalculate
}
if (cmdIdx >= 0) {
String start = prefix.substring(0, cmdIdx);
String end = null;
if (cmdIdx + CMD.length() >= prefix.length()) {
end = "";
} else {
end = prefix.substring(cmdIdx + CMD.length());
}
prefix = start + cmd + end;
}
return prefix;
}
/**
* Returns the full path to the executable in <code>$GEMFIRE/bin</code> taking into account the
* {@linkplain ManagedEntityConfig#getProductDirectory product directory} and the platform's file
* separator.
*
* <P>
*
* Note: we should probably do a better job of determine whether or not the machine on which the
* entity runs is Windows or Linux.
*
* @param executable The name of the executable that resides in <code>$GEMFIRE/bin</code>.
*/
@Override
public String getProductExecutable(InternalManagedEntity entity, String executable) {
String productDirectory = entity.getEntityConfig().getProductDirectory();
String path = null;
File productDir = new File(productDirectory);
{
path = productDir.getPath();
if (!endsWithSeparator(path)) {
path += File.separator;
}
path += "bin" + File.separator;
}
String bat = "";
if (pathIsWindows(path)) {
bat = ".bat";
}
return getOSPath(path) + executable + bat;
}
/**
* Builds optional SSL command-line arguments. Returns null if SSL is not enabled for the
* distributed system.
*/
@Override
public String buildSSLArguments(DistributedSystemConfig config) {
Properties sslProps = buildSSLProperties(config, true);
if (sslProps == null)
return null;
StringBuffer sb = new StringBuffer();
for (Iterator iter = sslProps.keySet().iterator(); iter.hasNext();) {
String key = (String) iter.next();
String value = sslProps.getProperty(key);
sb.append(" -J-D" + key + "=" + value);
}
return sb.toString();
}
/**
* Builds optional SSL properties for DistributionLocator. Returns null if SSL is not enabled for
* the distributed system.
*
* @param forCommandLine true indicates that {@link DistributionConfig#GEMFIRE_PREFIX} should be
* prepended so the argument will become -Dgemfire.xxxx
*/
private Properties buildSSLProperties(DistributedSystemConfig config, boolean forCommandLine) {
if (!config.isSSLEnabled())
return null;
String prefix = "";
if (forCommandLine)
prefix = DistributionConfig.GEMFIRE_PREFIX;
Properties sslProps = (Properties) config.getSSLProperties().clone();
// add ssl-enabled, etc...
sslProps.setProperty(prefix + MCAST_PORT, "0");
sslProps.setProperty(prefix + CLUSTER_SSL_ENABLED, String.valueOf(config.isSSLEnabled()));
sslProps.setProperty(prefix + CLUSTER_SSL_CIPHERS, config.getSSLCiphers());
sslProps.setProperty(prefix + CLUSTER_SSL_PROTOCOLS, config.getSSLProtocols());
sslProps.setProperty(prefix + CLUSTER_SSL_REQUIRE_AUTHENTICATION,
String.valueOf(config.isSSLAuthenticationRequired()));
return sslProps;
}
/**
* Starts a managed entity.
*/
@Override
public void start(final InternalManagedEntity entity) {
final String command = arrangeRemoteCommand(entity, entity.getStartCommand());
Thread start = new LoggingThread("Start " + entity.getEntityType(),
false, () -> execute(command, entity));
start.start();
}
/**
* Stops a managed entity.
*/
@Override
public void stop(final InternalManagedEntity entity) {
final String command = arrangeRemoteCommand(entity, entity.getStopCommand());
Thread stop = new LoggingThread("Stop " + entity.getEntityType(),
false, () -> execute(command, entity));
stop.start();
}
/**
* Returns whether or not a managed entity is running
*/
@Override
public boolean isRunning(InternalManagedEntity entity) {
final String command = arrangeRemoteCommand(entity, entity.getIsRunningCommand());
String output = execute(command, entity);
if (output == null || (output.indexOf("stop" /* "ing" "ped" */) != -1)
|| (output.indexOf("killed") != -1) || (output.indexOf("starting") != -1)) {
return false;
} else if (output.indexOf("running") != -1) {
return true;
} else {
throw new IllegalStateException(
String.format("Could not determine if managed entity was running: %s",
output));
}
}
/**
* Returns the contents of a locator's log file. Other APIs are used to get the log file of
* managed entities that are also system members.
*/
@Override
public String getLog(DistributionLocatorImpl locator) {
String command = arrangeRemoteCommand(locator, locator.getLogCommand());
return execute(command, locator);
}
/**
* Returns the contents of the given directory using the given managed entity to determine the
* host and remote command.
*/
private String listDirectory(InternalManagedEntity entity, String dir) {
ManagedEntityConfig config = entity.getEntityConfig();
String listFile = pathIsWindows(config.getProductDirectory()) ? "dir " : "ls ";
String command = arrangeRemoteCommand(entity, listFile + dir);
return execute(command, entity);
}
}