/*
* 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.cli.commands;
import static org.apache.geode.distributed.ConfigurationProperties.*;
import static org.apache.geode.test.dunit.Assert.*;
import static org.apache.geode.test.dunit.LogWriterUtils.*;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.geode.security.TestSecurityManager;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.apache.geode.cache.Cache;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.management.ManagementService;
import org.apache.geode.management.internal.cli.CommandManager;
import org.apache.geode.management.internal.cli.HeadlessGfsh;
import org.apache.geode.management.internal.cli.i18n.CliStrings;
import org.apache.geode.management.internal.cli.parser.CommandTarget;
import org.apache.geode.management.internal.cli.result.CommandResult;
import org.apache.geode.management.internal.cli.shell.Gfsh;
import org.apache.geode.management.internal.cli.util.CommandStringBuilder;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.rules.DistributedRestoreSystemProperties;
/**
* Base class for all the CLI/gfsh command dunit tests.
*/
public abstract class CliCommandTestBase extends JUnit4CacheTestCase {
public static final String USE_HTTP_SYSTEM_PROPERTY = "useHTTP";
private boolean useHttpOnConnect = Boolean.getBoolean(USE_HTTP_SYSTEM_PROPERTY);
private ManagementService managementService;
private transient HeadlessGfsh shell;
protected transient int httpPort;
protected transient int jmxPort;
protected transient String jmxHost;
protected transient String gfshDir;
@Rule
public transient DistributedRestoreSystemProperties restoreSystemProperties =
new DistributedRestoreSystemProperties();
@Rule
public transient TemporaryFolder temporaryFolder = new TemporaryFolder();
@Override
public final void postSetUp() throws Exception {
setUpCliCommandTestBase();
postSetUpCliCommandTestBase();
}
private void setUpCliCommandTestBase() throws Exception {
this.gfshDir = this.temporaryFolder.newFolder("gfsh_files").getCanonicalPath();
}
protected void postSetUpCliCommandTestBase() throws Exception {}
@Override
public final void preTearDownCacheTestCase() throws Exception {
preTearDownCliCommandTestBase();
destroyDefaultSetup();
}
protected void preTearDownCliCommandTestBase() throws Exception {}
/**
* Create all of the components necessary for the default setup. The provided properties will be
* used when creating the default cache. This will create GFSH in the controller VM (VM[4]) (no
* cache) and the manager in VM[0] (with cache). When adding regions, functions, keys, whatever to
* your cache for tests, you'll need to use Host.getHost(0).getVM(0).invoke(new
* SerializableRunnable() { public void run() { ... } } in order to have this setup run in the
* same VM as the manager.
*
* @param props the Properties used when creating the cache for this default setup.
* @return the default testable GemFire shell.
*/
@SuppressWarnings("serial")
protected HeadlessGfsh setUpJmxManagerOnVm0ThenConnect(final Properties props) {
Object[] result = setUpJMXManagerOnVM(0, props);
this.jmxHost = (String) result[0];
this.jmxPort = (Integer) result[1];
this.httpPort = (Integer) result[2];
connect(this.jmxHost, this.jmxPort, this.httpPort, getDefaultShell());
return shell;
}
protected Object[] setUpJMXManagerOnVM(int vm, final Properties props) {
return setUpJMXManagerOnVM(vm, props, null);
}
/**
* @return an object array, result[0] is jmxHost(String), result[1] is jmxPort, result[2] is
* httpPort
*/
protected Object[] setUpJMXManagerOnVM(int vm, final Properties props, String jsonFile) {
Object[] result = Host.getHost(0).getVM(vm).invoke("setUpJmxManagerOnVm" + vm, () -> {
final Object[] results = new Object[3];
final Properties localProps = (props != null ? props : new Properties());
try {
jmxHost = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException ignore) {
jmxHost = "localhost";
}
if (!localProps.containsKey(NAME)) {
localProps.setProperty(NAME, "Manager");
}
if (jsonFile != null) {
localProps.setProperty(TestSecurityManager.SECURITY_JSON, jsonFile);
}
final int[] ports = AvailablePortHelper.getRandomAvailableTCPPorts(2);
jmxPort = ports[0];
httpPort = ports[1];
localProps.setProperty(JMX_MANAGER, "true");
localProps.setProperty(JMX_MANAGER_START, "true");
localProps.setProperty(JMX_MANAGER_BIND_ADDRESS, String.valueOf(jmxHost));
localProps.setProperty(JMX_MANAGER_PORT, String.valueOf(jmxPort));
localProps.setProperty(HTTP_SERVICE_PORT, String.valueOf(httpPort));
getSystem(localProps);
verifyManagementServiceStarted(getCache());
IgnoredException.addIgnoredException("org.eclipse.jetty.io.EofException");
IgnoredException.addIgnoredException("java.nio.channels.ClosedChannelException");
results[0] = jmxHost;
results[1] = jmxPort;
results[2] = httpPort;
return results;
});
return result;
}
/**
* Destroy all of the components created for the default setup.
*/
protected final void destroyDefaultSetup() {
if (this.shell != null) {
executeCommand(shell, "exit");
this.shell.terminate();
this.shell = null;
}
disconnectAllFromDS();
Host.getHost(0).getVM(0).invoke("verify service stopped",
() -> verifyManagementServiceStopped());
}
/**
* Start the default management service using the provided Cache.
*
* @param cache Cache to use when creating the management service
*/
private void verifyManagementServiceStarted(Cache cache) {
assertTrue(cache != null);
this.managementService = ManagementService.getExistingManagementService(cache);
assertNotNull(this.managementService);
assertTrue(this.managementService.isManager());
assertTrue(checkIfCommandsAreLoadedOrNot());
}
public static boolean checkIfCommandsAreLoadedOrNot() {
CommandManager manager;
try {
manager = CommandManager.getInstance();
Map<String, CommandTarget> commands = manager.getCommands();
if (commands.size() < 1) {
return false;
}
return true;
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException("Could not load commands", e);
}
}
/**
* Stop the default management service.
*/
private void verifyManagementServiceStopped() {
if (this.managementService != null) {
assertFalse(this.managementService.isManager());
this.managementService = null;
}
}
protected void connect(final String host, final int jmxPort, final int httpPort,
HeadlessGfsh shell) {
connect(host, jmxPort, httpPort, shell, null, null);
}
protected void connect(final String host, final int jmxPort, final int httpPort,
HeadlessGfsh shell, String username, String password) {
final CommandStringBuilder command = new CommandStringBuilder(CliStrings.CONNECT);
String endpoint;
if (useHttpOnConnect) {
endpoint = "http://" + host + ":" + httpPort + "/gemfire/v1";
command.addOption(CliStrings.CONNECT__USE_HTTP, Boolean.TRUE.toString());
command.addOption(CliStrings.CONNECT__URL, endpoint);
} else {
endpoint = host + "[" + jmxPort + "]";
command.addOption(CliStrings.CONNECT__JMX_MANAGER, endpoint);
}
if (username != null) {
command.addOption(CliStrings.CONNECT__USERNAME, username);
}
if (password != null) {
command.addOption(CliStrings.CONNECT__PASSWORD, password);
}
System.out.println(getClass().getSimpleName() + " using endpoint: " + endpoint);
CommandResult result = executeCommand(shell, command.toString());
if (!shell.isConnectedAndReady()) {
throw new AssertionError("Connect command failed to connect to manager " + endpoint
+ " result=" + commandResultToString(result));
}
info("Successfully connected to managing node using " + (useHttpOnConnect ? "HTTP" : "JMX"));
assertEquals(true, shell.isConnectedAndReady());
}
/**
* Get the default shell (will create one if it doesn't already exist).
*
* @return The default shell
*/
protected synchronized final HeadlessGfsh getDefaultShell() {
if (this.shell == null) {
this.shell = createShell();
}
return this.shell;
}
/**
* Create a HeadlessGfsh object.
*
* @return The created shell.
*/
protected HeadlessGfsh createShell() {
try {
Gfsh.SUPPORT_MUTLIPLESHELL = true;
String shellId = getClass().getSimpleName() + "_" + getName();
HeadlessGfsh shell = new HeadlessGfsh(shellId, 30, this.gfshDir);
// Added to avoid trimming of the columns
info("Started testable shell: " + shell);
return shell;
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Execute a command using the default shell and clear the shell events before returning.
*
* @param command Command to execute
* @return The result of the command execution
*/
protected CommandResult executeCommand(String command) {
assert (command != null);
return executeCommand(getDefaultShell(), command);
}
/**
* Execute a command in the provided shell and clear the shell events before returning.
*
* @param shell Shell in which to execute the command.
* @param command Command to execute
* @return The result of the command execution
*/
protected CommandResult executeCommand(HeadlessGfsh shell, String command) {
assert (shell != null);
assert (command != null);
CommandResult commandResult = executeCommandWithoutClear(shell, command);
shell.clearEvents();
return commandResult;
}
/**
* Execute a command using the default shell. Useful for getting additional information from the
* shell after the command has been executed (using getDefaultShell().???). Caller is responsible
* for calling getDefaultShell().clearEvents() when done.
*
* @param command Command to execute
* @return The result of the command execution
*/
@SuppressWarnings("unused")
protected CommandResult executeCommandWithoutClear(String command) {
assert (command != null);
return executeCommandWithoutClear(getDefaultShell(), command);
}
/**
* Execute a command in the provided shell. Useful for getting additional information from the
* shell after the command has been executed (using getDefaultShell().???). Caller is responsible
* for calling getDefaultShell().clearEvents() when done.
*
* @param shell Shell in which to execute the command.
* @param command Command to execute
* @return The result of the command execution
*/
protected CommandResult executeCommandWithoutClear(HeadlessGfsh shell, String command) {
assert (shell != null);
assert (command != null);
try {
info("Executing command " + command + " with command Mgr " + CommandManager.getInstance());
} catch (ClassNotFoundException cnfex) {
throw new AssertionError(cnfex);
} catch (IOException ioex) {
throw new AssertionError(ioex);
}
shell.executeCommand(command);
if (shell.hasError()) {
error("executeCommand completed with error : " + shell.getError());
}
CommandResult result = null;
try {
result = (CommandResult) shell.getResult(); // TODO: this can result in ClassCastException if
// command resulted in error
} catch (InterruptedException ex) {
error("shell received InterruptedException");
}
if (result != null) {
result.resetToFirstLine();
}
return result;
}
/**
* Utility method for viewing the results of a command.
*
* @param commandResult Results to dump
* @param printStream Stream to dump the results to
*/
protected void printResult(final CommandResult commandResult, PrintStream printStream) {
assert (commandResult != null);
assert (printStream != null);
commandResult.resetToFirstLine();
printStream.print(commandResultToString(commandResult));
}
protected static String commandResultToString(final CommandResult commandResult) {
assertNotNull(commandResult);
commandResult.resetToFirstLine();
StringBuilder buffer = new StringBuilder(commandResult.getHeader());
while (commandResult.hasNextLine()) {
buffer.append(commandResult.nextLine());
}
buffer.append(commandResult.getFooter());
return buffer.toString();
}
/**
* Utility method for finding the CommandResult object in the Map of CommandOutput objects.
*
* @param commandOutput CommandOutput Map to search
* @return The CommandResult object or null if not found.
*/
protected CommandResult extractCommandResult(Map<String, Object> commandOutput) {
assert (commandOutput != null);
for (Object resultObject : commandOutput.values()) {
if (resultObject instanceof CommandResult) {
CommandResult result = (CommandResult) resultObject;
result.resetToFirstLine();
return result;
}
}
return null;
}
/**
* Utility method to determine how many times a string occurs in another string. Note that when
* looking for matches substrings of other matches will be counted as a match. For example,
* looking for "AA" in the string "AAAA" will result in a return value of 3.
*
* @param stringToSearch String to search
* @param stringToCount String to look for and count
* @return The number of matches.
*/
protected int countMatchesInString(final String stringToSearch, final String stringToCount) {
assert (stringToSearch != null);
assert (stringToCount != null);
int length = stringToSearch.length();
int count = 0;
for (int i = 0; i < length; i++) {
if (stringToSearch.substring(i).startsWith(stringToCount)) {
count++;
}
}
return count;
}
/**
* Determines if a string contains a trimmed line that matches the pattern. So, any single line
* whose leading and trailing spaces have been removed which contains a string that exactly
* matches the given pattern will be considered a match.
*
* @param stringToSearch String to search
* @param stringPattern Pattern to search for
* @return True if a match is found, false otherwise
*/
protected boolean stringContainsLine(final String stringToSearch, final String stringPattern) {
assert (stringToSearch != null);
assert (stringPattern != null);
Pattern pattern = Pattern.compile("^\\s*" + stringPattern + "\\s*$", Pattern.MULTILINE);
Matcher matcher = pattern.matcher(stringToSearch);
return matcher.find();
}
/**
* Counts the number of distinct lines in a String.
*
* @param stringToSearch String to search for lines.
* @param countBlankLines Whether to count blank lines (true to count)
* @return The number of lines found.
*/
protected int countLinesInString(final String stringToSearch, final boolean countBlankLines) {
assert (stringToSearch != null);
int length = stringToSearch.length();
int count = 0;
char character = 0;
boolean foundNonSpaceChar = false;
for (int i = 0; i < length; i++) {
character = stringToSearch.charAt(i);
if (character == '\r' && (i + 1) < length && stringToSearch.charAt(i + 1) == '\n') {
i++;
}
if (character == '\n' || character == '\r') {
if (countBlankLines) {
count++;
} else {
if (foundNonSpaceChar) {
count++;
}
}
foundNonSpaceChar = false;
} else if (character != ' ' && character != '\t') {
foundNonSpaceChar = true;
}
}
// Even if the last line isn't terminated, it still counts as a line
if (character != '\n' && character != '\r') {
count++;
}
return count;
}
/**
* Get a specific line from the string (using \n or \r as a line separator).
*
* @param stringToSearch String to get the line from
* @param lineNumber Line number to get
* @return The line
*/
protected String getLineFromString(final String stringToSearch, final int lineNumber) {
assert (stringToSearch != null);
assert (lineNumber > 0);
int length = stringToSearch.length();
int count = 0;
int startIndex = 0;
char character;
int endIndex = length;
for (int i = 0; i < length; i++) {
character = stringToSearch.charAt(i);
if (character == '\r' && (i + 1) < length && stringToSearch.charAt(i + 1) == '\n') {
i++;
}
if (character == '\n' || character == '\r') {
if (lineNumber == 1) {
endIndex = i;
break;
}
if (++count == lineNumber - 1) {
startIndex = i + 1;
} else if (count >= lineNumber) {
endIndex = i;
break;
}
}
}
return stringToSearch.substring(startIndex, endIndex);
}
protected void info(String string) {
getLogWriter().info(string);
}
protected void debug(String string) {
getLogWriter().fine(string);
}
protected void error(String string) {
getLogWriter().error(string);
}
protected void error(String string, Throwable e) {
getLogWriter().error(string, e);
}
}