blob: 7678ead43bf354ad80caf289ce40a901029b005c [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.accumulo.shell;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.ClientConfiguration;
import org.apache.accumulo.core.client.ClientConfiguration.ClientProperty;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.Instance;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.NamespaceNotFoundException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.ZooKeeperInstance;
import org.apache.accumulo.core.client.impl.ClientContext;
import org.apache.accumulo.core.client.impl.Tables;
import org.apache.accumulo.core.client.security.tokens.AuthenticationToken;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.conf.SiteConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.data.thrift.TConstraintViolationSummary;
import org.apache.accumulo.core.tabletserver.thrift.ConstraintViolationException;
import org.apache.accumulo.core.trace.DistributedTrace;
import org.apache.accumulo.core.util.BadArgumentException;
import org.apache.accumulo.core.util.DeprecationUtil;
import org.apache.accumulo.core.util.format.DefaultFormatter;
import org.apache.accumulo.core.util.format.Formatter;
import org.apache.accumulo.core.util.format.FormatterConfig;
import org.apache.accumulo.core.util.format.FormatterFactory;
import org.apache.accumulo.core.volume.VolumeConfiguration;
import org.apache.accumulo.core.zookeeper.ZooUtil;
import org.apache.accumulo.shell.commands.AboutCommand;
import org.apache.accumulo.shell.commands.AddAuthsCommand;
import org.apache.accumulo.shell.commands.AddSplitsCommand;
import org.apache.accumulo.shell.commands.AuthenticateCommand;
import org.apache.accumulo.shell.commands.ByeCommand;
import org.apache.accumulo.shell.commands.ClasspathCommand;
import org.apache.accumulo.shell.commands.ClearCommand;
import org.apache.accumulo.shell.commands.CloneTableCommand;
import org.apache.accumulo.shell.commands.ClsCommand;
import org.apache.accumulo.shell.commands.CompactCommand;
import org.apache.accumulo.shell.commands.ConfigCommand;
import org.apache.accumulo.shell.commands.ConstraintCommand;
import org.apache.accumulo.shell.commands.CreateNamespaceCommand;
import org.apache.accumulo.shell.commands.CreateTableCommand;
import org.apache.accumulo.shell.commands.CreateUserCommand;
import org.apache.accumulo.shell.commands.DUCommand;
import org.apache.accumulo.shell.commands.DebugCommand;
import org.apache.accumulo.shell.commands.DeleteAuthsCommand;
import org.apache.accumulo.shell.commands.DeleteCommand;
import org.apache.accumulo.shell.commands.DeleteIterCommand;
import org.apache.accumulo.shell.commands.DeleteManyCommand;
import org.apache.accumulo.shell.commands.DeleteNamespaceCommand;
import org.apache.accumulo.shell.commands.DeleteRowsCommand;
import org.apache.accumulo.shell.commands.DeleteScanIterCommand;
import org.apache.accumulo.shell.commands.DeleteShellIterCommand;
import org.apache.accumulo.shell.commands.DeleteTableCommand;
import org.apache.accumulo.shell.commands.DeleteUserCommand;
import org.apache.accumulo.shell.commands.DropTableCommand;
import org.apache.accumulo.shell.commands.DropUserCommand;
import org.apache.accumulo.shell.commands.EGrepCommand;
import org.apache.accumulo.shell.commands.ExecfileCommand;
import org.apache.accumulo.shell.commands.ExitCommand;
import org.apache.accumulo.shell.commands.ExportTableCommand;
import org.apache.accumulo.shell.commands.ExtensionCommand;
import org.apache.accumulo.shell.commands.FateCommand;
import org.apache.accumulo.shell.commands.FlushCommand;
import org.apache.accumulo.shell.commands.FormatterCommand;
import org.apache.accumulo.shell.commands.GetAuthsCommand;
import org.apache.accumulo.shell.commands.GetGroupsCommand;
import org.apache.accumulo.shell.commands.GetSplitsCommand;
import org.apache.accumulo.shell.commands.GrantCommand;
import org.apache.accumulo.shell.commands.GrepCommand;
import org.apache.accumulo.shell.commands.HelpCommand;
import org.apache.accumulo.shell.commands.HiddenCommand;
import org.apache.accumulo.shell.commands.HistoryCommand;
import org.apache.accumulo.shell.commands.ImportDirectoryCommand;
import org.apache.accumulo.shell.commands.ImportTableCommand;
import org.apache.accumulo.shell.commands.InfoCommand;
import org.apache.accumulo.shell.commands.InsertCommand;
import org.apache.accumulo.shell.commands.InterpreterCommand;
import org.apache.accumulo.shell.commands.ListBulkCommand;
import org.apache.accumulo.shell.commands.ListCompactionsCommand;
import org.apache.accumulo.shell.commands.ListIterCommand;
import org.apache.accumulo.shell.commands.ListScansCommand;
import org.apache.accumulo.shell.commands.ListShellIterCommand;
import org.apache.accumulo.shell.commands.MaxRowCommand;
import org.apache.accumulo.shell.commands.MergeCommand;
import org.apache.accumulo.shell.commands.NamespacePermissionsCommand;
import org.apache.accumulo.shell.commands.NamespacesCommand;
import org.apache.accumulo.shell.commands.NoTableCommand;
import org.apache.accumulo.shell.commands.OfflineCommand;
import org.apache.accumulo.shell.commands.OnlineCommand;
import org.apache.accumulo.shell.commands.OptUtil;
import org.apache.accumulo.shell.commands.PasswdCommand;
import org.apache.accumulo.shell.commands.PingCommand;
import org.apache.accumulo.shell.commands.QuestionCommand;
import org.apache.accumulo.shell.commands.QuitCommand;
import org.apache.accumulo.shell.commands.QuotedStringTokenizer;
import org.apache.accumulo.shell.commands.RenameNamespaceCommand;
import org.apache.accumulo.shell.commands.RenameTableCommand;
import org.apache.accumulo.shell.commands.RevokeCommand;
import org.apache.accumulo.shell.commands.ScanCommand;
import org.apache.accumulo.shell.commands.ScriptCommand;
import org.apache.accumulo.shell.commands.SetAuthsCommand;
import org.apache.accumulo.shell.commands.SetGroupsCommand;
import org.apache.accumulo.shell.commands.SetIterCommand;
import org.apache.accumulo.shell.commands.SetScanIterCommand;
import org.apache.accumulo.shell.commands.SetShellIterCommand;
import org.apache.accumulo.shell.commands.SleepCommand;
import org.apache.accumulo.shell.commands.SystemPermissionsCommand;
import org.apache.accumulo.shell.commands.TableCommand;
import org.apache.accumulo.shell.commands.TablePermissionsCommand;
import org.apache.accumulo.shell.commands.TablesCommand;
import org.apache.accumulo.shell.commands.TraceCommand;
import org.apache.accumulo.shell.commands.UserCommand;
import org.apache.accumulo.shell.commands.UserPermissionsCommand;
import org.apache.accumulo.shell.commands.UsersCommand;
import org.apache.accumulo.shell.commands.WhoAmICommand;
import org.apache.accumulo.start.classloader.vfs.AccumuloVFSClassLoader;
import org.apache.accumulo.start.classloader.vfs.ContextManager;
import org.apache.accumulo.start.spi.KeywordExecutable;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.hadoop.fs.Path;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.auto.service.AutoService;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.history.FileHistory;
/**
* A convenient console interface to perform basic accumulo functions Includes auto-complete, help, and quoted strings with escape sequences
*/
@AutoService(KeywordExecutable.class)
public class Shell extends ShellOptions implements KeywordExecutable {
public static final Logger log = Logger.getLogger(Shell.class);
private static final Logger audit = Logger.getLogger(Shell.class.getName() + ".audit");
public static final Charset CHARSET = ISO_8859_1;
public static final int NO_FIXED_ARG_LENGTH_CHECK = -1;
public static final String COMMENT_PREFIX = "#";
public static final String HISTORY_DIR_NAME = ".accumulo";
public static final String HISTORY_FILE_NAME = "shell_history.txt";
private static final String SHELL_DESCRIPTION = "Shell - Apache Accumulo Interactive Shell";
protected int exitCode = 0;
private String tableName;
protected Instance instance;
private Connector connector;
protected ConsoleReader reader;
private AuthenticationToken token;
private final Class<? extends Formatter> defaultFormatterClass = DefaultFormatter.class;
public Map<String,List<IteratorSetting>> scanIteratorOptions = new HashMap<>();
public Map<String,List<IteratorSetting>> iteratorProfiles = new HashMap<>();
private Token rootToken;
public final Map<String,Command> commandFactory = new TreeMap<>();
public final Map<String,Command[]> commandGrouping = new TreeMap<>();
// exit if true
private boolean exit = false;
// file to execute commands from
protected File execFile = null;
// single command to execute from the command line
protected String execCommand = null;
protected boolean verbose = true;
private boolean tabCompletion;
private boolean disableAuthTimeout;
private long authTimeout;
private long lastUserActivity = System.nanoTime();
private boolean logErrorsToConsole = false;
private boolean masking = false;
{
// set the JLine output encoding to some reasonable default if it isn't already set
// despite the misleading property name, "input.encoding" is the property jline uses for the encoding of the output stream writer
String prop = "input.encoding";
if (System.getProperty(prop) == null) {
String value = System.getProperty("jline.WindowsTerminal.output.encoding");
if (value == null) {
value = System.getProperty("file.encoding");
}
if (value != null) {
System.setProperty(prop, value);
}
}
}
// no arg constructor should do minimal work since its used in Main ServiceLoader
public Shell() {}
public Shell(ConsoleReader reader) {
super();
this.reader = reader;
}
/**
* Configures the shell using the provided options. Not for client use.
*
* @return true if the shell was successfully configured, false otherwise.
* @throws IOException
* if problems occur creating the ConsoleReader
*/
public boolean config(String... args) throws IOException {
if (this.reader == null)
this.reader = new ConsoleReader();
ShellOptionsJC options = new ShellOptionsJC();
JCommander jc = new JCommander();
jc.setProgramName("accumulo shell");
jc.addObject(options);
try {
jc.parse(args);
} catch (ParameterException e) {
jc.usage();
exitCode = 1;
return false;
}
if (options.isHelpEnabled()) {
jc.usage();
// Not an error
exitCode = 0;
return false;
}
if (options.getUnrecognizedOptions() != null) {
logError("Unrecognized Options: " + options.getUnrecognizedOptions().toString());
jc.usage();
exitCode = 1;
return false;
}
setDebugging(options.isDebugEnabled());
authTimeout = TimeUnit.MINUTES.toNanos(options.getAuthTimeout());
disableAuthTimeout = options.isAuthTimeoutDisabled();
ClientConfiguration clientConf;
try {
clientConf = options.getClientConfiguration();
} catch (Exception e) {
printException(e);
return true;
}
if (Boolean.parseBoolean(clientConf.get(ClientProperty.INSTANCE_RPC_SASL_ENABLED))) {
log.debug("SASL is enabled, disabling authorization timeout");
disableAuthTimeout = true;
}
// get the options that were parsed
final String user;
try {
user = options.getUsername();
} catch (Exception e) {
printException(e);
return true;
}
String password = options.getPassword();
tabCompletion = !options.isTabCompletionDisabled();
// Use a ZK, or HdfsZK Accumulo instance
setInstance(options);
// AuthenticationToken options
try {
token = options.getAuthenticationToken();
} catch (Exception e) {
printException(e);
return true;
}
Map<String,String> loginOptions = options.getTokenProperties();
// process default parameters if unspecified
try {
final boolean hasToken = (token != null);
if (hasToken && password != null) {
throw new ParameterException("Can not supply '--pass' option with '--tokenClass' option");
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
reader.getTerminal().setEchoEnabled(true);
}
});
if (hasToken) { // implied hasTokenOptions
// Fully qualified name so we don't shadow java.util.Properties
org.apache.accumulo.core.client.security.tokens.AuthenticationToken.Properties props;
// and line wrap it because the package name is so long
props = new org.apache.accumulo.core.client.security.tokens.AuthenticationToken.Properties();
if (!loginOptions.isEmpty()) {
props.putAllStrings(loginOptions);
}
token.init(props);
} else {
// Read password if the user explicitly asked for it, or didn't specify anything at all
if ("stdin".equals(password) || password == null) {
password = reader.readLine("Password: ", '*');
}
if (password == null) {
// User cancel, e.g. Ctrl-D pressed
throw new ParameterException("No password or token option supplied");
} else {
this.token = new PasswordToken(password);
}
}
if (!options.isFake()) {
DistributedTrace.enable(InetAddress.getLocalHost().getHostName(), "shell", clientConf);
}
this.setTableName("");
connector = instance.getConnector(user, token);
} catch (Exception e) {
printException(e);
exitCode = 1;
return false;
}
// decide whether to execute commands from a file and quit
if (options.getExecFile() != null) {
execFile = options.getExecFile();
verbose = false;
} else if (options.getExecFileVerbose() != null) {
execFile = options.getExecFileVerbose();
verbose = true;
}
execCommand = options.getExecCommand();
if (execCommand != null) {
verbose = false;
}
rootToken = new Token();
Command[] dataCommands = {new DeleteCommand(), new DeleteManyCommand(), new DeleteRowsCommand(), new EGrepCommand(), new FormatterCommand(),
new InterpreterCommand(), new GrepCommand(), new ImportDirectoryCommand(), new InsertCommand(), new MaxRowCommand(), new ScanCommand()};
Command[] debuggingCommands = {new ClasspathCommand(), new DebugCommand(), new ListScansCommand(), new ListCompactionsCommand(), new TraceCommand(),
new PingCommand(), new ListBulkCommand()};
Command[] execCommands = {new ExecfileCommand(), new HistoryCommand(), new ExtensionCommand(), new ScriptCommand()};
Command[] exitCommands = {new ByeCommand(), new ExitCommand(), new QuitCommand()};
Command[] helpCommands = {new AboutCommand(), new HelpCommand(), new InfoCommand(), new QuestionCommand()};
Command[] iteratorCommands = {new DeleteIterCommand(), new DeleteScanIterCommand(), new ListIterCommand(), new SetIterCommand(), new SetScanIterCommand(),
new SetShellIterCommand(), new ListShellIterCommand(), new DeleteShellIterCommand()};
Command[] otherCommands = {new HiddenCommand()};
Command[] permissionsCommands = {new GrantCommand(), new RevokeCommand(), new SystemPermissionsCommand(), new TablePermissionsCommand(),
new UserPermissionsCommand(), new NamespacePermissionsCommand()};
Command[] stateCommands = {new AuthenticateCommand(), new ClsCommand(), new ClearCommand(), new FateCommand(), new NoTableCommand(), new SleepCommand(),
new TableCommand(), new UserCommand(), new WhoAmICommand()};
Command[] tableCommands = {new CloneTableCommand(), new ConfigCommand(), new CreateTableCommand(), new DeleteTableCommand(), new DropTableCommand(),
new DUCommand(), new ExportTableCommand(), new ImportTableCommand(), new OfflineCommand(), new OnlineCommand(), new RenameTableCommand(),
new TablesCommand(), new NamespacesCommand(), new CreateNamespaceCommand(), new DeleteNamespaceCommand(), new RenameNamespaceCommand()};
Command[] tableControlCommands = {new AddSplitsCommand(), new CompactCommand(), new ConstraintCommand(), new FlushCommand(), new GetGroupsCommand(),
new GetSplitsCommand(), new MergeCommand(), new SetGroupsCommand()};
Command[] userCommands = {new AddAuthsCommand(), new CreateUserCommand(), new DeleteUserCommand(), new DropUserCommand(), new GetAuthsCommand(),
new PasswdCommand(), new SetAuthsCommand(), new UsersCommand(), new DeleteAuthsCommand()};
commandGrouping.put("-- Writing, Reading, and Removing Data --", dataCommands);
commandGrouping.put("-- Debugging Commands -------------------", debuggingCommands);
commandGrouping.put("-- Shell Execution Commands -------------", execCommands);
commandGrouping.put("-- Exiting Commands ---------------------", exitCommands);
commandGrouping.put("-- Help Commands ------------------------", helpCommands);
commandGrouping.put("-- Iterator Configuration ---------------", iteratorCommands);
commandGrouping.put("-- Permissions Administration Commands --", permissionsCommands);
commandGrouping.put("-- Shell State Commands -----------------", stateCommands);
commandGrouping.put("-- Table Administration Commands --------", tableCommands);
commandGrouping.put("-- Table Control Commands ---------------", tableControlCommands);
commandGrouping.put("-- User Administration Commands ---------", userCommands);
for (Command[] cmds : commandGrouping.values()) {
for (Command cmd : cmds)
commandFactory.put(cmd.getName(), cmd);
}
for (Command cmd : otherCommands) {
commandFactory.put(cmd.getName(), cmd);
}
return true;
}
/**
* Sets the instance used by the shell based on the given options.
*
* @param options
* shell options
*/
protected void setInstance(ShellOptionsJC options) {
// should only be one set of instance options set
instance = null;
if (options.isFake()) {
instance = DeprecationUtil.makeMockInstance("fake");
} else {
String instanceName, hosts;
if (options.isHdfsZooInstance()) {
instanceName = hosts = null;
} else if (options.getZooKeeperInstance().size() > 0) {
List<String> zkOpts = options.getZooKeeperInstance();
instanceName = zkOpts.get(0);
hosts = zkOpts.get(1);
} else {
instanceName = options.getZooKeeperInstanceName();
hosts = options.getZooKeeperHosts();
}
final ClientConfiguration clientConf;
try {
clientConf = options.getClientConfiguration();
} catch (ConfigurationException | FileNotFoundException e) {
throw new IllegalArgumentException("Unable to load client config from " + options.getClientConfigFile(), e);
}
instance = getZooInstance(instanceName, hosts, clientConf);
}
}
/**
* Get the ZooKeepers. Use the value passed in (if there was one), then fall back to the ClientConf, finally trying the accumulo-site.xml.
*
* @param keepers
* ZooKeepers passed to the shell
* @param clientConfig
* ClientConfiguration instance
* @return The ZooKeepers to connect to
*/
static String getZooKeepers(String keepers, ClientConfiguration clientConfig, AccumuloConfiguration conf) {
if (null != keepers) {
return keepers;
}
if (clientConfig.containsKey(ClientProperty.INSTANCE_ZK_HOST.getKey())) {
return clientConfig.get(ClientProperty.INSTANCE_ZK_HOST);
}
return conf.get(Property.INSTANCE_ZK_HOST);
}
/*
* Takes instanceName and keepers as separate arguments, rather than just packaged into the clientConfig, so that we can fail over to accumulo-site.xml or
* HDFS config if they're unspecified.
*/
private static Instance getZooInstance(String instanceName, String keepersOption, ClientConfiguration clientConfig) {
UUID instanceId = null;
if (instanceName == null) {
instanceName = clientConfig.get(ClientProperty.INSTANCE_NAME);
}
AccumuloConfiguration conf = SiteConfiguration.getInstance(ClientContext.convertClientConfig(clientConfig));
String keepers = getZooKeepers(keepersOption, clientConfig, conf);
if (instanceName == null) {
Path instanceDir = new Path(VolumeConfiguration.getVolumeUris(conf)[0], "instance_id");
instanceId = UUID.fromString(ZooUtil.getInstanceIDFromHdfs(instanceDir, conf));
}
if (instanceId != null) {
return new ZooKeeperInstance(clientConfig.withInstance(instanceId).withZkHosts(keepers));
} else {
return new ZooKeeperInstance(clientConfig.withInstance(instanceName).withZkHosts(keepers));
}
}
public Connector getConnector() {
return connector;
}
public Instance getInstance() {
return instance;
}
public ClassLoader getClassLoader(final CommandLine cl, final Shell shellState) throws AccumuloException, TableNotFoundException, AccumuloSecurityException,
IOException, FileSystemException {
boolean tables = cl.hasOption(OptUtil.tableOpt().getOpt()) || !shellState.getTableName().isEmpty();
boolean namespaces = cl.hasOption(OptUtil.namespaceOpt().getOpt());
String classpath = null;
Iterable<Entry<String,String>> tableProps;
if (namespaces) {
try {
tableProps = shellState.getConnector().namespaceOperations().getProperties(OptUtil.getNamespaceOpt(cl, shellState));
} catch (NamespaceNotFoundException e) {
throw new IllegalArgumentException(e);
}
} else if (tables) {
tableProps = shellState.getConnector().tableOperations().getProperties(OptUtil.getTableOpt(cl, shellState));
} else {
throw new IllegalArgumentException("No table or namespace specified");
}
for (Entry<String,String> entry : tableProps) {
if (entry.getKey().equals(Property.TABLE_CLASSPATH.getKey())) {
classpath = entry.getValue();
}
}
ClassLoader classloader;
if (classpath != null && !classpath.equals("")) {
shellState.getConnector().instanceOperations().getSystemConfiguration().get(Property.VFS_CONTEXT_CLASSPATH_PROPERTY.getKey() + classpath);
try {
AccumuloVFSClassLoader.getContextManager().setContextConfig(new ContextManager.DefaultContextsConfig(new Iterable<Map.Entry<String,String>>() {
@Override
public Iterator<Entry<String,String>> iterator() {
try {
return shellState.getConnector().instanceOperations().getSystemConfiguration().entrySet().iterator();
} catch (AccumuloException e) {
throw new RuntimeException(e);
} catch (AccumuloSecurityException e) {
throw new RuntimeException(e);
}
}
}));
} catch (IllegalStateException ise) {}
classloader = AccumuloVFSClassLoader.getContextManager().getClassLoader(classpath);
} else {
classloader = AccumuloVFSClassLoader.getClassLoader();
}
return classloader;
}
@Override
public String keyword() {
return "shell";
}
@Override
public void execute(final String[] args) throws IOException {
try {
if (!config(args)) {
System.exit(getExitCode());
}
System.exit(start());
} finally {
shutdown();
DistributedTrace.disable();
}
}
public static void main(String args[]) throws IOException {
new Shell(new ConsoleReader()).execute(args);
}
public int start() throws IOException {
String input;
if (isVerbose())
printInfo();
String home = System.getProperty("HOME");
if (home == null)
home = System.getenv("HOME");
String configDir = home + "/" + HISTORY_DIR_NAME;
String historyPath = configDir + "/" + HISTORY_FILE_NAME;
File accumuloDir = new File(configDir);
if (!accumuloDir.exists() && !accumuloDir.mkdirs())
log.warn("Unable to make directory for history at " + accumuloDir);
try {
final FileHistory history = new FileHistory(new File(historyPath));
reader.setHistory(history);
// Add shutdown hook to flush file history, per jline javadocs
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
history.flush();
} catch (IOException e) {
log.warn("Could not flush history to file.");
}
}
});
} catch (IOException e) {
log.warn("Unable to load history file at " + historyPath);
}
// Turn Ctrl+C into Exception instead of JVM exit
reader.setHandleUserInterrupt(true);
ShellCompletor userCompletor = null;
if (execFile != null) {
java.util.Scanner scanner = new java.util.Scanner(execFile, UTF_8.name());
try {
while (scanner.hasNextLine() && !hasExited()) {
execCommand(scanner.nextLine(), true, isVerbose());
}
} finally {
scanner.close();
}
} else if (execCommand != null) {
for (String command : execCommand.split("\n")) {
execCommand(command, true, isVerbose());
}
return exitCode;
}
while (true) {
try {
if (hasExited())
return exitCode;
// If tab completion is true we need to reset
if (tabCompletion) {
if (userCompletor != null)
reader.removeCompleter(userCompletor);
userCompletor = setupCompletion();
reader.addCompleter(userCompletor);
}
reader.setPrompt(getDefaultPrompt());
input = reader.readLine();
if (input == null) {
reader.println();
return exitCode;
} // User Canceled (Ctrl+D)
execCommand(input, disableAuthTimeout, false);
} catch (UserInterruptException uie) {
// User Cancelled (Ctrl+C)
reader.println();
String partialLine = uie.getPartialLine();
if (partialLine == null || "".equals(uie.getPartialLine().trim())) {
// No content, actually exit
return exitCode;
}
} finally {
reader.flush();
}
}
}
public void shutdown() {
if (reader != null) {
reader.shutdown();
}
}
public void printInfo() throws IOException {
reader.print("\n" + SHELL_DESCRIPTION + "\n" + "- \n" + "- version: " + Constants.VERSION + "\n" + "- instance name: "
+ connector.getInstance().getInstanceName() + "\n" + "- instance id: " + connector.getInstance().getInstanceID() + "\n" + "- \n"
+ "- type 'help' for a list of available commands\n" + "- \n");
reader.flush();
}
public void printVerboseInfo() throws IOException {
StringBuilder sb = new StringBuilder("-\n");
sb.append("- Current user: ").append(connector.whoami()).append("\n");
if (execFile != null)
sb.append("- Executing commands from: ").append(execFile).append("\n");
if (disableAuthTimeout)
sb.append("- Authorization timeout: disabled\n");
else
sb.append("- Authorization timeout: ").append(String.format("%ds%n", TimeUnit.NANOSECONDS.toSeconds(authTimeout)));
sb.append("- Debug: ").append(isDebuggingEnabled() ? "on" : "off").append("\n");
if (!scanIteratorOptions.isEmpty()) {
for (Entry<String,List<IteratorSetting>> entry : scanIteratorOptions.entrySet()) {
sb.append("- Session scan iterators for table ").append(entry.getKey()).append(":\n");
for (IteratorSetting setting : entry.getValue()) {
sb.append("- Iterator ").append(setting.getName()).append(" options:\n");
sb.append("- ").append("iteratorPriority").append(" = ").append(setting.getPriority()).append("\n");
sb.append("- ").append("iteratorClassName").append(" = ").append(setting.getIteratorClass()).append("\n");
for (Entry<String,String> optEntry : setting.getOptions().entrySet()) {
sb.append("- ").append(optEntry.getKey()).append(" = ").append(optEntry.getValue()).append("\n");
}
}
}
}
sb.append("-\n");
reader.print(sb.toString());
}
public String getDefaultPrompt() {
return connector.whoami() + "@" + connector.getInstance().getInstanceName() + (getTableName().isEmpty() ? "" : " ") + getTableName() + "> ";
}
public void execCommand(String input, boolean ignoreAuthTimeout, boolean echoPrompt) throws IOException {
audit.log(Level.INFO, getDefaultPrompt() + input);
if (echoPrompt) {
reader.print(getDefaultPrompt());
reader.println(input);
}
if (input.startsWith(COMMENT_PREFIX)) {
return;
}
String fields[];
try {
fields = new QuotedStringTokenizer(input).getTokens();
} catch (BadArgumentException e) {
printException(e);
++exitCode;
return;
}
if (fields.length == 0)
return;
String command = fields[0];
fields = fields.length > 1 ? Arrays.copyOfRange(fields, 1, fields.length) : new String[] {};
Command sc = null;
if (command.length() > 0) {
try {
// Obtain the command from the command table
sc = commandFactory.get(command);
if (sc == null) {
reader.println(String.format("Unknown command \"%s\". Enter \"help\" for a list possible commands.", command));
reader.flush();
return;
}
long duration = System.nanoTime() - lastUserActivity;
if (!(sc instanceof ExitCommand) && !ignoreAuthTimeout && (duration < 0 || duration > authTimeout)) {
reader.println("Shell has been idle for too long. Please re-authenticate.");
boolean authFailed = true;
do {
String pwd = readMaskedLine("Enter current password for '" + connector.whoami() + "': ", '*');
if (pwd == null) {
reader.println();
return;
} // user canceled
try {
authFailed = !connector.securityOperations().authenticateUser(connector.whoami(), new PasswordToken(pwd));
} catch (Exception e) {
++exitCode;
printException(e);
}
if (authFailed)
reader.print("Invalid password. ");
} while (authFailed);
lastUserActivity = System.nanoTime();
}
// Get the options from the command on how to parse the string
Options parseOpts = sc.getOptionsWithHelp();
// Parse the string using the given options
CommandLine cl = new BasicParser().parse(parseOpts, fields);
int actualArgLen = cl.getArgs().length;
int expectedArgLen = sc.numArgs();
if (cl.hasOption(helpOption)) {
// Display help if asked to; otherwise execute the command
sc.printHelp(this);
} else if (expectedArgLen != NO_FIXED_ARG_LENGTH_CHECK && actualArgLen != expectedArgLen) {
++exitCode;
// Check for valid number of fixed arguments (if not
// negative; negative means it is not checked, for
// vararg-like commands)
printException(new IllegalArgumentException(String.format("Expected %d argument%s. There %s %d.", expectedArgLen, expectedArgLen == 1 ? "" : "s",
actualArgLen == 1 ? "was" : "were", actualArgLen)));
sc.printHelp(this);
} else {
int tmpCode = sc.execute(input, cl, this);
exitCode += tmpCode;
reader.flush();
}
} catch (ConstraintViolationException e) {
++exitCode;
printConstraintViolationException(e);
} catch (TableNotFoundException e) {
++exitCode;
if (getTableName().equals(e.getTableName()))
setTableName("");
printException(e);
} catch (ParseException e) {
// not really an error if the exception is a missing required
// option when the user is asking for help
if (!(e instanceof MissingOptionException && (Arrays.asList(fields).contains("-" + helpOption) || Arrays.asList(fields).contains("--" + helpLongOption)))) {
++exitCode;
printException(e);
}
if (sc != null)
sc.printHelp(this);
} catch (UserInterruptException e) {
++exitCode;
} catch (Exception e) {
++exitCode;
printException(e);
}
} else {
++exitCode;
printException(new BadArgumentException("Unrecognized empty command", command, -1));
}
reader.flush();
}
/**
* The command tree is built in reverse so that the references are more easily linked up. There is some code in token to allow forward building of the command
* tree.
*/
private ShellCompletor setupCompletion() {
rootToken = new Token();
Set<String> tableNames = null;
try {
tableNames = connector.tableOperations().list();
} catch (Exception e) {
log.debug("Unable to obtain list of tables", e);
tableNames = Collections.emptySet();
}
Set<String> userlist = null;
try {
userlist = connector.securityOperations().listLocalUsers();
} catch (Exception e) {
log.debug("Unable to obtain list of users", e);
userlist = Collections.emptySet();
}
Set<String> namespaces = null;
try {
namespaces = connector.namespaceOperations().list();
} catch (Exception e) {
log.debug("Unable to obtain list of namespaces", e);
namespaces = Collections.emptySet();
}
Map<Command.CompletionSet,Set<String>> options = new HashMap<>();
Set<String> commands = new HashSet<>();
for (String a : commandFactory.keySet())
commands.add(a);
Set<String> modifiedUserlist = new HashSet<>();
Set<String> modifiedTablenames = new HashSet<>();
Set<String> modifiedNamespaces = new HashSet<>();
for (String a : tableNames)
modifiedTablenames.add(a.replaceAll("([\\s'\"])", "\\\\$1"));
for (String a : userlist)
modifiedUserlist.add(a.replaceAll("([\\s'\"])", "\\\\$1"));
for (String a : namespaces) {
String b = a.replaceAll("([\\s'\"])", "\\\\$1");
modifiedNamespaces.add(b.isEmpty() ? "\"\"" : b);
}
options.put(Command.CompletionSet.USERNAMES, modifiedUserlist);
options.put(Command.CompletionSet.TABLENAMES, modifiedTablenames);
options.put(Command.CompletionSet.NAMESPACES, modifiedNamespaces);
options.put(Command.CompletionSet.COMMANDS, commands);
for (Command[] cmdGroup : commandGrouping.values()) {
for (Command c : cmdGroup) {
c.getOptionsWithHelp(); // prep the options for the command
// so that the completion can
// include them
c.registerCompletion(rootToken, options);
}
}
return new ShellCompletor(rootToken, options);
}
/**
* The Command class represents a command to be run in the shell. It contains the methods to execute along with some methods to help tab completion, and
* return the command name, help, and usage.
*/
public static abstract class Command {
// Helper methods for completion
public enum CompletionSet {
TABLENAMES, USERNAMES, COMMANDS, NAMESPACES
}
public void registerCompletionGeneral(Token root, Set<String> args, boolean caseSens) {
Token t = new Token(args);
t.setCaseSensitive(caseSens);
Token command = new Token(getName());
command.addSubcommand(t);
root.addSubcommand(command);
}
public void registerCompletionForTables(Token root, Map<CompletionSet,Set<String>> completionSet) {
registerCompletionGeneral(root, completionSet.get(CompletionSet.TABLENAMES), true);
}
public void registerCompletionForUsers(Token root, Map<CompletionSet,Set<String>> completionSet) {
registerCompletionGeneral(root, completionSet.get(CompletionSet.USERNAMES), true);
}
public void registerCompletionForCommands(Token root, Map<CompletionSet,Set<String>> completionSet) {
registerCompletionGeneral(root, completionSet.get(CompletionSet.COMMANDS), false);
}
public void registerCompletionForNamespaces(Token root, Map<CompletionSet,Set<String>> completionSet) {
registerCompletionGeneral(root, completionSet.get(CompletionSet.NAMESPACES), true);
}
// abstract methods to override
public abstract int execute(String fullCommand, CommandLine cl, Shell shellState) throws Exception;
public abstract String description();
/**
* If the number of arguments is not always zero (not including those arguments handled through Options), make sure to override the {@link #usage()} method.
* Otherwise, {@link #usage()} does need to be overridden.
*/
public abstract int numArgs();
// OPTIONAL methods to override:
// the general version of getname uses reflection to get the class name
// and then cuts off the suffix -Command to get the name of the command
public String getName() {
String s = this.getClass().getName();
int st = Math.max(s.lastIndexOf('$'), s.lastIndexOf('.'));
int i = s.indexOf("Command");
return i > 0 ? s.substring(st + 1, i).toLowerCase(Locale.ENGLISH) : null;
}
// The general version of this method adds the name
// of the command to the completion tree
public void registerCompletion(Token root, Map<CompletionSet,Set<String>> completion_set) {
root.addSubcommand(new Token(getName()));
}
// The general version of this method uses the HelpFormatter
// that comes with the apache Options package to print out the help
public final void printHelp(Shell shellState) throws IOException {
shellState.printHelp(usage(), "description: " + this.description(), getOptionsWithHelp());
}
public final void printHelp(Shell shellState, int width) throws IOException {
shellState.printHelp(usage(), "description: " + this.description(), getOptionsWithHelp(), width);
}
// Get options with help
public final Options getOptionsWithHelp() {
Options opts = getOptions();
opts.addOption(new Option(helpOption, helpLongOption, false, "display this help"));
return opts;
}
// General usage is just the command
public String usage() {
return getName();
}
// General Options are empty
public Options getOptions() {
return new Options();
}
}
public interface PrintLine {
void print(String s);
void close();
}
public static class PrintShell implements PrintLine {
ConsoleReader reader;
public PrintShell(ConsoleReader reader) {
this.reader = reader;
}
@Override
public void print(String s) {
try {
reader.println(s);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public void close() {}
}
public static class PrintFile implements PrintLine {
PrintWriter writer;
public PrintFile(String filename) throws FileNotFoundException {
writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename), UTF_8)));
}
@Override
public void print(String s) {
writer.println(s);
}
@Override
public void close() {
writer.close();
}
}
public final void printLines(Iterator<String> lines, boolean paginate) throws IOException {
printLines(lines, paginate, null);
}
public final void printLines(Iterator<String> lines, boolean paginate, PrintLine out) throws IOException {
int linesPrinted = 0;
String prompt = "-- hit any key to continue or 'q' to quit --";
int lastPromptLength = prompt.length();
int termWidth = reader.getTerminal().getWidth();
int maxLines = reader.getTerminal().getHeight();
String peek = null;
while (lines.hasNext()) {
String nextLine = lines.next();
if (nextLine == null)
continue;
for (String line : nextLine.split("\\n")) {
if (out == null) {
if (peek != null) {
reader.println(peek);
if (paginate) {
linesPrinted += peek.length() == 0 ? 0 : Math.ceil(peek.length() * 1.0 / termWidth);
// check if displaying the next line would result in
// scrolling off the screen
if (linesPrinted + Math.ceil(lastPromptLength * 1.0 / termWidth) + Math.ceil(prompt.length() * 1.0 / termWidth)
+ Math.ceil(line.length() * 1.0 / termWidth) > maxLines) {
linesPrinted = 0;
int numdashes = (termWidth - prompt.length()) / 2;
String nextPrompt = repeat("-", numdashes) + prompt + repeat("-", numdashes);
lastPromptLength = nextPrompt.length();
reader.print(nextPrompt);
reader.flush();
if (Character.toUpperCase((char) reader.readCharacter()) == 'Q') {
reader.println();
return;
}
reader.println();
termWidth = reader.getTerminal().getWidth();
maxLines = reader.getTerminal().getHeight();
}
}
}
peek = line;
} else {
out.print(line);
}
}
}
if (out == null && peek != null) {
reader.println(peek);
}
}
public final void printRecords(Iterable<Entry<Key,Value>> scanner, FormatterConfig config, boolean paginate, Class<? extends Formatter> formatterClass,
PrintLine outFile) throws IOException {
printLines(FormatterFactory.getFormatter(formatterClass, scanner, config), paginate, outFile);
}
public final void printRecords(Iterable<Entry<Key,Value>> scanner, FormatterConfig config, boolean paginate, Class<? extends Formatter> formatterClass)
throws IOException {
printLines(FormatterFactory.getFormatter(formatterClass, scanner, config), paginate);
}
public static String repeat(String s, int c) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < c; i++)
sb.append(s);
return sb.toString();
}
public void checkTableState() {
if (getTableName().isEmpty())
throw new IllegalStateException(
"Not in a table context. Please use 'table <tableName>' to switch to a table, or use '-t' to specify a table if option is available.");
}
private final void printConstraintViolationException(ConstraintViolationException cve) {
printException(cve, "");
int COL1 = 50, COL2 = 14;
int col3 = Math.max(1, Math.min(Integer.MAX_VALUE, reader.getTerminal().getWidth() - COL1 - COL2 - 6));
logError(String.format("%" + COL1 + "s-+-%" + COL2 + "s-+-%" + col3 + "s%n", repeat("-", COL1), repeat("-", COL2), repeat("-", col3)));
logError(String.format("%-" + COL1 + "s | %" + COL2 + "s | %-" + col3 + "s%n", "Constraint class", "Violation code", "Violation Description"));
logError(String.format("%" + COL1 + "s-+-%" + COL2 + "s-+-%" + col3 + "s%n", repeat("-", COL1), repeat("-", COL2), repeat("-", col3)));
for (TConstraintViolationSummary cvs : cve.violationSummaries)
logError(String.format("%-" + COL1 + "s | %" + COL2 + "d | %-" + col3 + "s%n", cvs.constrainClass, cvs.violationCode, cvs.violationDescription));
logError(String.format("%" + COL1 + "s-+-%" + COL2 + "s-+-%" + col3 + "s%n", repeat("-", COL1), repeat("-", COL2), repeat("-", col3)));
}
public final void printException(Exception e) {
printException(e, e.getMessage());
}
private final void printException(Exception e, String msg) {
logError(e.getClass().getName() + (msg != null ? ": " + msg : ""));
log.debug(e.getClass().getName() + (msg != null ? ": " + msg : ""), e);
}
public static final void setDebugging(boolean debuggingEnabled) {
Logger.getLogger(Constants.CORE_PACKAGE_NAME).setLevel(debuggingEnabled ? Level.TRACE : Level.INFO);
Logger.getLogger(Shell.class.getPackage().getName()).setLevel(debuggingEnabled ? Level.TRACE : Level.INFO);
}
public static final boolean isDebuggingEnabled() {
return Logger.getLogger(Constants.CORE_PACKAGE_NAME).isTraceEnabled();
}
private final void printHelp(String usage, String description, Options opts) throws IOException {
printHelp(usage, description, opts, Integer.MAX_VALUE);
}
private final void printHelp(String usage, String description, Options opts, int width) throws IOException {
new HelpFormatter().printHelp(new PrintWriter(reader.getOutput()), width, usage, description, opts, 2, 5, null, true);
reader.getOutput().flush();
}
public int getExitCode() {
return exitCode;
}
public void resetExitCode() {
exitCode = 0;
}
public void setExit(boolean exit) {
this.exit = exit;
}
public boolean getExit() {
return this.exit;
}
public boolean isVerbose() {
return verbose;
}
public void setTableName(String tableName) {
this.tableName = (tableName == null || tableName.isEmpty()) ? "" : Tables.qualified(tableName);
}
public String getTableName() {
return tableName;
}
public ConsoleReader getReader() {
return reader;
}
public void updateUser(String principal, AuthenticationToken token) throws AccumuloException, AccumuloSecurityException {
connector = instance.getConnector(principal, token);
this.token = token;
}
public String getPrincipal() {
return connector.whoami();
}
public AuthenticationToken getToken() {
return token;
}
/**
* Return the formatter for the current table.
*
* @return the formatter class for the current table
*/
public Class<? extends Formatter> getFormatter() {
return getFormatter(this.tableName);
}
/**
* Return the formatter for the given table.
*
* @param tableName
* the table name
* @return the formatter class for the given table
*/
public Class<? extends Formatter> getFormatter(String tableName) {
Class<? extends Formatter> formatter = FormatterCommand.getCurrentFormatter(tableName, this);
if (null == formatter) {
logError("Could not load the specified formatter. Using the DefaultFormatter");
return this.defaultFormatterClass;
} else {
return formatter;
}
}
public void setLogErrorsToConsole() {
this.logErrorsToConsole = true;
}
private void logError(String s) {
log.error(s);
if (logErrorsToConsole) {
try {
reader.println("ERROR: " + s);
reader.flush();
} catch (IOException e) {}
}
}
public String readMaskedLine(String prompt, Character mask) throws IOException {
this.masking = true;
String s = reader.readLine(prompt, mask);
this.masking = false;
return s;
}
public boolean isMasking() {
return masking;
}
public boolean hasExited() {
return exit;
}
public boolean isTabCompletion() {
return tabCompletion;
}
}