/* * 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.cassandra.tools; import java.io.*; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; import com.google.common.base.Joiner; import com.google.common.base.Throwables; import com.google.common.collect.*; import io.airlift.command.*; import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.tools.nodetool.*; import org.apache.cassandra.utils.FBUtilities; import static com.google.common.base.Throwables.getStackTraceAsString; import static com.google.common.collect.Iterables.toArray; import static com.google.common.collect.Lists.newArrayList; import static java.lang.Integer.parseInt; import static java.lang.String.format; import static java.util.Arrays.asList; import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY; import static org.apache.commons.lang3.StringUtils.*; public class NodeTool { private static final String HISTORYFILE = "nodetool.history"; public static void main(String... args) { List<Class<? extends Runnable>> commands = asList( Help.class, Info.class, Ring.class, NetStats.class, CfStats.class, TableStats.class, // New name CfHistograms.class, TableHistograms.class, // New name Cleanup.class, ClearSnapshot.class, Compact.class, // Remove for GA: Scrub.class, // Remove until proven otherwise: Verify.class, Flush.class, // Remove for GA: UpgradeSSTable.class, // Remove for GA: DisableAutoCompaction.class, // Remove for GA: EnableAutoCompaction.class, CompactionStats.class, CompactionHistory.class, Decommission.class, DescribeCluster.class, DisableBinary.class, EnableBinary.class, EnableGossip.class, DisableGossip.class, // Remove for GA: EnableHandoff.class, // Remove for GA: EnableThrift.class, // Remove for GA: GcStats.class, // Remove for GA: GetCompactionThreshold.class, // Remove for GA: GetCompactionThroughput.class, // Remove for GA: GetStreamThroughput.class, // Remove until proven otherwise: EnableHandoff.class, // Remove until proven otherwise: EnableThrift.class, // Remove until proven otherwise: GcStats.class, // Remove until proven otherwise: GetCompactionThreshold.class, // Remove until proven otherwise: GetCompactionThroughput.class, // Remove until proven otherwise: GetStreamThroughput.class, // Remove until proven otherwise: GetTraceProbability.class, // Remove until proven otherwise: GetInterDCStreamThroughput.class, GetEndpoints.class, // Remove for GA: GetSSTables.class, GossipInfo.class, // Remove for GA: InvalidateKeyCache.class, // Remove for GA: InvalidateRowCache.class, // Remove for GA: InvalidateCounterCache.class, // Remove for GA: Join.class, Move.class, // Remove for GA: PauseHandoff.class, // Remove for GA: ResumeHandoff.class, ProxyHistograms.class, Rebuild.class, Refresh.class, // Remove for GA: RemoveToken.class, RemoveNode.class, // Remove until proven otherwise: Assassinate.class, Repair.class, // Remove for GA: SetCacheCapacity.class, // Remove for GA: SetHintedHandoffThrottleInKB.class, // Remove for GA: SetCompactionThreshold.class, // Remove for GA: SetCompactionThroughput.class, // Remove for GA: SetStreamThroughput.class, // Remove until proven otherwise: ReplayBatchlog.class, // Remove until proven otherwise: SetCacheCapacity.class, // Remove until proven otherwise: SetHintedHandoffThrottleInKB.class, // Remove until proven otherwise: SetCompactionThreshold.class, // Remove until proven otherwise: SetCompactionThroughput.class, // Remove until proven otherwise: SetStreamThroughput.class, // Remove until proven otherwise: SetInterDCStreamThroughput.class, SetTraceProbability.class, Snapshot.class, ListSnapshots.class, Status.class, StatusBinary.class, StatusGossip.class, // Remove for GA: StatusThrift.class, StatusBackup.class, // Remove for GA: StatusHandoff.class, Stop.class, // Remove for GA: StopDaemon.class, Version.class, DescribeRing.class, // Remove for GA: RebuildIndex.class, // Remove for GA: RangeKeySample.class, EnableBackup.class, DisableBackup.class, // Remove for GA: ResetLocalSchema.class, // Remove for GA: ReloadTriggers.class, // Remove for GA: SetCacheKeysToSave.class, // Remove for GA: DisableThrift.class, // Remove for GA: DisableHandoff.class, Drain.class, // Remove for GA: TruncateHints.class, // Remove for GA: TpStats.class, // Remove for GA: TopPartitions.class, SetLoggingLevel.class, GetLoggingLevels.class // Remove until proven otherwise: DisableHintsForDC.class, // Remove until proven otherwise: EnableHintsForDC.class, // Remove until proven otherwise: FailureDetectorInfo.class, // Remove until proven otherwise: RefreshSizeEstimates.class ); Cli.CliBuilder<Runnable> builder = Cli.builder("nodetool"); builder.withDescription("Manage your Cassandra cluster") .withDefaultCommand(Help.class) .withCommands(commands); // bootstrap commands builder.withGroup("bootstrap") .withDescription("Monitor/manage node's bootstrap process") .withDefaultCommand(Help.class) .withCommand(BootstrapResume.class); Cli<Runnable> parser = builder.build(); int status = 0; try { Runnable parse = parser.parse(args); printHistory(args); parse.run(); } catch (IllegalArgumentException | IllegalStateException | ParseArgumentsMissingException | ParseArgumentsUnexpectedException | ParseOptionConversionException | ParseOptionMissingException | ParseOptionMissingValueException | ParseCommandMissingException | ParseCommandUnrecognizedException e) { badUse(e); status = 1; } catch (CommandFailedButNeedNoMoreOutput e) { status = 1; } catch (Throwable throwable) { err(Throwables.getRootCause(throwable)); status = 2; } System.exit(status); } private static void printHistory(String... args) { //don't bother to print if no args passed (meaning, nodetool is just printing out the sub-commands list) if (args.length == 0) return; String cmdLine = Joiner.on(" ").skipNulls().join(args); cmdLine = cmdLine.replaceFirst("(?<=(-pw|--password))\\s+\\S+", " <hidden>"); try (FileWriter writer = new FileWriter(new File(FBUtilities.getToolsOutputDirectory(), HISTORYFILE), true)) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); writer.append(sdf.format(new Date())).append(": ").append(cmdLine).append(System.lineSeparator()); } catch (IOException | IOError ioe) { //quietly ignore any errors about not being able to write out history } } private static void badUse(Exception e) { System.out.println("nodetool: " + e.getMessage()); System.out.println("See 'nodetool help' or 'nodetool help <command>'."); } private static void err(Throwable e) { System.err.println("error: " + e.getMessage()); System.err.println("-- StackTrace --"); System.err.println(getStackTraceAsString(e)); } @SuppressWarnings("serial") public static class CommandFailedButNeedNoMoreOutput extends Error {}; public static abstract class NodeToolCmd implements Runnable { @Option(type = OptionType.GLOBAL, name = {"-h", "--host"}, description = "Node hostname or ip address") private String host = "127.0.0.1"; @Option(type = OptionType.GLOBAL, name = {"-p", "--port"}, description = "Remote jmx agent port number") private String port = "7199"; @Option(type = OptionType.GLOBAL, name = {"-u", "--username"}, description = "Remote jmx agent username") private String username = EMPTY; @Option(type = OptionType.GLOBAL, name = {"-pw", "--password"}, description = "Remote jmx agent password") private String password = EMPTY; @Option(type = OptionType.GLOBAL, name = {"-pwf", "--password-file"}, description = "Path to the JMX password file") private String passwordFilePath = EMPTY; @Override public void run() { if (isNotEmpty(username)) { if (isNotEmpty(passwordFilePath)) password = readUserPasswordFromFile(username, passwordFilePath); if (isEmpty(password)) password = promptAndReadPassword(); } try (NodeProbe probe = connect()) { execute(probe); if (probe.isFailed()) throw new RuntimeException("nodetool failed, check server logs"); } catch (IOException e) { throw new RuntimeException("Error while closing JMX connection", e); } } private String readUserPasswordFromFile(String username, String passwordFilePath) { String password = EMPTY; File passwordFile = new File(passwordFilePath); try (Scanner scanner = new Scanner(passwordFile).useDelimiter("\\s+")) { while (scanner.hasNextLine()) { if (scanner.hasNext()) { String jmxRole = scanner.next(); if (jmxRole.equals(username) && scanner.hasNext()) { password = scanner.next(); break; } } scanner.nextLine(); } } catch (FileNotFoundException e) { throw new RuntimeException(e); } return password; } private String promptAndReadPassword() { String password = EMPTY; Console console = System.console(); if (console != null) password = String.valueOf(console.readPassword("Password:")); return password; } protected abstract void execute(NodeProbe probe); private NodeProbe connect() { NodeProbe nodeClient = null; try { if (username.isEmpty()) nodeClient = new NodeProbe(host, parseInt(port)); else nodeClient = new NodeProbe(host, parseInt(port), username, password); } catch (IOException e) { Throwable rootCause = Throwables.getRootCause(e); System.err.println(format("nodetool: Failed to connect to '%s:%s' - %s: '%s'.", host, port, rootCause.getClass().getSimpleName(), rootCause.getMessage())); System.exit(1); } return nodeClient; } protected enum KeyspaceSet { ALL, NON_SYSTEM, NON_LOCAL_STRATEGY } protected List<String> parseOptionalKeyspace(List<String> cmdArgs, NodeProbe nodeProbe) { return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.NON_SYSTEM); } protected List<String> parseOptionalKeyspace(List<String> cmdArgs, NodeProbe nodeProbe, KeyspaceSet defaultKeyspaceSet) { List<String> keyspaces = new ArrayList<>(); if (cmdArgs == null || cmdArgs.isEmpty()) { if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY) keyspaces.addAll(keyspaces = nodeProbe.getNonLocalStrategyKeyspaces()); else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM) keyspaces.addAll(keyspaces = nodeProbe.getNonSystemKeyspaces()); else keyspaces.addAll(nodeProbe.getKeyspaces()); } else { keyspaces.add(cmdArgs.get(0)); } for (String keyspace : keyspaces) { if (!nodeProbe.getKeyspaces().contains(keyspace)) throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist."); } return Collections.unmodifiableList(keyspaces); } protected String[] parseOptionalTables(List<String> cmdArgs) { return cmdArgs.size() <= 1 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(1, cmdArgs.size()), String.class); } } public static SortedMap<String, SetHostStat> getOwnershipByDc(NodeProbe probe, boolean resolveIp, Map<String, String> tokenToEndpoint, Map<InetAddress, Float> ownerships) { SortedMap<String, SetHostStat> ownershipByDc = Maps.newTreeMap(); EndpointSnitchInfoMBean epSnitchInfo = probe.getEndpointSnitchInfoProxy(); try { for (Entry<String, String> tokenAndEndPoint : tokenToEndpoint.entrySet()) { String dc = epSnitchInfo.getDatacenter(tokenAndEndPoint.getValue()); if (!ownershipByDc.containsKey(dc)) ownershipByDc.put(dc, new SetHostStat(resolveIp)); ownershipByDc.get(dc).add(tokenAndEndPoint.getKey(), tokenAndEndPoint.getValue(), ownerships); } } catch (UnknownHostException e) { throw new RuntimeException(e); } return ownershipByDc; } }