diff --git a/.build/build-rat.xml b/.build/build-rat.xml
index 27e8f63ae43d..5ce5e0dbd72c 100644
--- a/.build/build-rat.xml
+++ b/.build/build-rat.xml
@@ -55,6 +55,7 @@
+
diff --git a/.build/cassandra-deps-template.xml b/.build/cassandra-deps-template.xml
index 437c0c31b5a9..32c7e4a2c0af 100644
--- a/.build/cassandra-deps-template.xml
+++ b/.build/cassandra-deps-template.xml
@@ -120,6 +120,14 @@
io.airliftairline
+
+ info.picocli
+ picocli
+
+
+ io.github.java-diff-utils
+ java-diff-utils
+ io.dropwizard.metricsmetrics-core
diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml
index 0c0e5d4ccc9e..23caee6fd61c 100644
--- a/.build/parent-pom-template.xml
+++ b/.build/parent-pom-template.xml
@@ -714,6 +714,16 @@
jbcrypt0.4
+
+ info.picocli
+ picocli
+ 4.7.5
+
+
+ io.github.java-diff-utils
+ java-diff-utils
+ 4.12
+ io.airliftairline
diff --git a/bin/nodetool b/bin/nodetool
index f78b02e34418..b1fb4f5a091d 100755
--- a/bin/nodetool
+++ b/bin/nodetool
@@ -98,13 +98,19 @@ if [ "x$MAX_HEAP_SIZE" = "x" ]; then
MAX_HEAP_SIZE="128m"
fi
+if [ "x$NODETOOL_RUNNER" = "x" ]; then
+ NODETOOL_RUNNER="org.apache.cassandra.tools.NodeTool"
+fi
+
+# shellcheck disable=SC2116
CMD=$(echo "$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE \
-XX:ParallelGCThreads=1 \
-Dcassandra.storagedir="$cassandra_storagedir" \
-Dcassandra.logdir="$CASSANDRA_LOG_DIR" \
-Dlogback.configurationFile=logback-tools.xml \
- $JVM_ARGS \
- org.apache.cassandra.tools.NodeTool -p $JMX_PORT $ARGS)
+ "$JVM_ARGS" \
+ $NODETOOL_RUNNER \
+ -p "$JMX_PORT" "$ARGS")
if [ "x$ARCHIVE_COMMAND" != "x" ]
then
diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
index e1f6d28d86a9..cfec7274f1f8 100644
--- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
+++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
@@ -77,6 +77,7 @@ public enum CassandraRelevantProperties
CACHEABLE_MUTATION_SIZE_LIMIT("cassandra.cacheable_mutation_size_limit_bytes", convertToString(1_000_000)),
CASSANDRA_ALLOW_SIMPLE_STRATEGY("cassandra.allow_simplestrategy"),
CASSANDRA_AVAILABLE_PROCESSORS("cassandra.available_processors"),
+ CASSANDRA_CLI_PICOCLI_LAYOUT("cassandra.cli.picocli.layout", "false"),
/** The classpath storage configuration file. */
CASSANDRA_CONFIG("cassandra.config", "cassandra.yaml"),
/**
diff --git a/src/java/org/apache/cassandra/management/BaseCommand.java b/src/java/org/apache/cassandra/management/BaseCommand.java
new file mode 100644
index 000000000000..71534320a7bb
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/BaseCommand.java
@@ -0,0 +1,62 @@
+/*
+ * 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.management;
+
+import javax.inject.Inject;
+
+import org.apache.cassandra.tools.Output;
+
+/**
+ * Base class for all nodetool commands.
+ */
+public abstract class BaseCommand implements Runnable
+{
+ @Inject
+ protected Output logger;
+
+ /** The ServiceBridge instance to interact with the Cassandra node. */
+ protected ServiceMBeanBridge bridge;
+
+ /**
+ * The ServiceBridge instance is injected by the picocli framework during command execution and is used to
+ * interact with the Cassandra node. This method is called by picocli and used depending on the execution strategy.
+ * @param bridge The ServiceBridge instance to inject.
+ */
+ public void setBridge(ServiceMBeanBridge bridge)
+ {
+ this.bridge = bridge;
+ }
+
+ /**
+ * Returns the ServiceBridge instance used to interact with the Cassandra node.
+ * @return The ServiceBridge instance.
+ */
+ public ServiceMBeanBridge getBridge()
+ {
+ return bridge;
+ }
+
+ @Override
+ public void run()
+ {
+ execute(bridge);
+ }
+
+ protected abstract void execute(ServiceMBeanBridge probe);
+}
diff --git a/src/java/org/apache/cassandra/management/CassandraHelpCommand.java b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java
new file mode 100644
index 000000000000..23ab472d19b9
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/CassandraHelpCommand.java
@@ -0,0 +1,152 @@
+/*
+ * 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.management;
+
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.google.common.base.Preconditions;
+
+import picocli.CommandLine;
+
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS;
+
+@CommandLine.Command(name = "help",
+ header = "Display help information about the specified command.",
+ synopsisHeading = "%nUsage: ",
+ helpCommand = true,
+ description = { "%nWhen no COMMAND is given, the usage help for the main command is displayed.",
+ "If a COMMAND is specified, the help for that command is shown.%n" })
+public class CassandraHelpCommand implements CommandLine.IHelpCommandInitializable2, Runnable
+{
+ @CommandLine.Option(names = { "--help" }, usageHelp = true, descriptionKey = "helpCommand.help",
+ description = "Show usage help for the help command and exit.")
+ private boolean helpRequested;
+
+ @CommandLine.Parameters(paramLabel = "command", arity = "0..1", descriptionKey = "helpCommand.command",
+ description = "The COMMAND to display the usage help message for.")
+ private String commands;
+
+ private CommandLine self;
+ private PrintWriter out;
+ private CommandLine.Help.ColorScheme colorScheme;
+
+ /**
+ * Invokes {@code #usage(PrintStream, CommandLine.Help.ColorScheme) usage} for the specified command,
+ * or for the parent command.
+ */
+ public void run()
+ {
+ CommandLine parent = self == null ? null : self.getParent();
+ if (parent == null)
+ return;
+
+ CommandLine.Help.ColorScheme colors = colorScheme == null ?
+ CommandLine.Help.defaultColorScheme(CommandLine.Help.Ansi.AUTO) :
+ colorScheme;
+
+ if (commands == null)
+ {
+ // If the parent command is the top-level command, print help for the top-level command.
+ printTopCommandUsage(parent, colors, out);
+ return;
+ }
+
+ Map parentSubcommands = parent.getCommandSpec().subcommands();
+ CommandLine subcommand = parentSubcommands.get(commands);
+
+ if (parent.isAbbreviatedSubcommandsAllowed())
+ throw new CommandLine.ParameterException(parent, "Abbreviated subcommands are not allowed.", null, commands);
+
+ if (subcommand == null)
+ throw new CommandLine.ParameterException(parent, "Unknown subcommand '" + commands + "'.", null, commands);
+
+ subcommand.usage(out, colors);
+ }
+
+ public static void printTopCommandUsage(CommandLine command, CommandLine.Help.ColorScheme colors, PrintWriter writer)
+ {
+ if (command == null)
+ return;
+
+ StringBuilder sb = new StringBuilder();
+ CommandLine.Help help = command.getHelpFactory().create(command.getCommandSpec(), colors);
+ if (!(help instanceof CassandraHelpLayout))
+ {
+ command.usage(writer, colors);
+ return;
+ }
+
+ Map helpSectionMap = cassandraTopLevelHelpSectionKeys((CassandraHelpLayout) help);
+ for (String key : command.getHelpSectionKeys())
+ {
+ CommandLine.IHelpSectionRenderer renderer = helpSectionMap.get(key);
+ if (renderer == null)
+ continue;
+ sb.append(renderer.render(help));
+ }
+
+ writer.println(sb);
+ writer.flush();
+ }
+
+ /**
+ * Top-level help command (includes all the available nodetool commands) has a different layout, so we need to
+ * provide a different set of keys for the help sections.
+ * @param layout The help class layout.
+ * @return Map of supported keys for the help sections.
+ */
+ public static Map cassandraTopLevelHelpSectionKeys(CassandraHelpLayout layout)
+ {
+ Map sectionMap = new LinkedHashMap<>();
+ sectionMap.put(SECTION_KEY_HEADER_HEADING, CommandLine.Help::headerHeading);
+ sectionMap.put(SECTION_KEY_HEADER, CommandLine.Help::header);
+ sectionMap.put(SECTION_KEY_SYNOPSIS, layout::topLevelSynopsis);
+ sectionMap.put(SECTION_KEY_COMMAND_LIST_HEADING, layout::topLevelCommandListHeading);
+ sectionMap.put(SECTION_KEY_COMMAND_LIST, CommandLine.Help::commandList);
+ sectionMap.put(SECTION_KEY_EXIT_CODE_LIST_HEADING, CommandLine.Help::exitCodeListHeading);
+ sectionMap.put(SECTION_KEY_EXIT_CODE_LIST, CommandLine.Help::exitCodeList);
+ sectionMap.put(SECTION_KEY_FOOTER_HEADING, CommandLine.Help::footerHeading);
+ sectionMap.put(SECTION_KEY_FOOTER, CommandLine.Help::footer);
+ return sectionMap;
+ }
+
+ /**
+ * The printHelpIfRequested method calls the init method on commands marked
+ * as helpCommand before the help command's run or call method is called.
+ */
+ public void init(CommandLine helpCommandLine,
+ CommandLine.Help.ColorScheme colorScheme,
+ PrintWriter out,
+ PrintWriter err)
+ {
+ this.self = Preconditions.checkNotNull(helpCommandLine, "helpCommandLine");
+ this.colorScheme = Preconditions.checkNotNull(colorScheme, "colorScheme");
+ this.out = Preconditions.checkNotNull(out, "outWriter");
+ }
+}
diff --git a/src/java/org/apache/cassandra/management/CassandraHelpLayout.java b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java
new file mode 100644
index 000000000000..c292ee152889
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/CassandraHelpLayout.java
@@ -0,0 +1,547 @@
+/*
+ * 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.management;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cassandra.utils.Pair;
+import picocli.CommandLine;
+
+import static org.apache.cassandra.management.CommandUtils.findBackwardCompatibleArgument;
+import static org.apache.cassandra.management.CommandUtils.leadingSpaces;
+import static org.apache.cassandra.management.CommandUtils.sortShortestFirst;
+import static org.apache.commons.lang3.ArrayUtils.isEmpty;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_END_OF_OPTIONS;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS;
+import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING;
+
+/**
+ * Help facotry for the Cassandra nodetool to generate the help output. This class is used to match
+ * the command output with the previously available nodetool help output format.
+ */
+public class CassandraHelpLayout extends CommandLine.Help
+{
+ public static final int DEFAULT_USAGE_HELP_WIDTH = 85;
+ private static final String DESCRIPTION_HEADING = "NAME%n";
+ private static final String SYNOPSIS_HEADING = "SYNOPSIS%n";
+ private static final String OPTIONS_HEADING = "OPTIONS%n";
+ private static final String FOOTER_HEADING = "%n";
+ private static final int DESCRIPTION_INDENT = 4;
+ public static final int COLUMN_INDENT = 8;
+ public static final int SUBCOMMANDS_INDENT = 4;
+ private static final CommandLine.Model.OptionSpec CASSANDRA_END_OF_OPTIONS_OPTION =
+ CommandLine.Model.OptionSpec.builder("--")
+ .description("This option can be used to separate command-line options from the " +
+ "list of argument, (useful when arguments might be mistaken for " +
+ "command-line options")
+ .arity("0")
+ .build();
+ public static final String TOP_LEVEL_SYNOPSIS_LIST_PREFIX = "usage:";
+ public static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:";
+ public static final String USAGE_HELP_FOOTER = "See 'nodetool help ' for more information on a specific command.";
+ public static final String SYNOPSIS_SUBCOMMANDS_LABEL = " []";
+ private static final String[] EMPTY_FOOTER = new String[0];
+
+ public CassandraHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme)
+ {
+ super(spec, scheme);
+ }
+
+ @Override
+ public String descriptionHeading(Object... params)
+ {
+ return createHeading(DESCRIPTION_HEADING, params);
+ }
+
+ /**
+ * @param params Arguments referenced by the format specifiers in the header strings
+ * @return the header string.
+ */
+ @Override
+ public String description(Object... params) {
+ CommandLine.Model.CommandSpec spec = commandSpec();
+ String fullName = spec.qualifiedName();
+
+ TextTable table = TextTable.forColumns(colorScheme(),
+ new Column(spec.usageMessage().width() - COLUMN_INDENT, COLUMN_INDENT,
+ Column.Overflow.WRAP));
+ table.setAdjustLineBreaksForWideCJKCharacters(spec.usageMessage().adjustLineBreaksForWideCJKCharacters());
+ table.indentWrappedLines = 0;
+
+ table.addRowValues(colorScheme().commandText(fullName)
+ .concat(" - ")
+ .concat(colorScheme().text(String.join(" ", spec.usageMessage().description()))));
+ table.addRowValues(Ansi.OFF.new Text("", colorScheme()));
+ return table.toString(new StringBuilder()).toString();
+ }
+
+ @Override
+ public String synopsisHeading(Object... params)
+ {
+ return createHeading(SYNOPSIS_HEADING, params);
+ }
+
+ @Override
+ public String synopsis(int synopsisHeadingLength)
+ {
+ return printDetailedSynopsis("", COLUMN_INDENT, true);
+ }
+
+ private Ansi.Text createCassandraSynopsisCommandText()
+ {
+ Ansi.Text commandText = ansi().new Text(0);
+ if (!commandSpec().subcommands().isEmpty())
+ return commandText.concat(SYNOPSIS_SUBCOMMANDS_LABEL);
+ return commandText;
+ }
+
+ private String printDetailedSynopsis(String synopsisPrefix, int columnIndent, boolean showEndOfOptionsDelimiter)
+ {
+ // Cassandra uses end of options delimiter in usage help.
+ commandSpec().usageMessage().showEndOfOptionsDelimiterInUsageHelp(showEndOfOptionsDelimiter);
+
+ CommandLine.Model.CommandSpec commandSpec = commandSpec();
+ ColorScheme colorScheme = colorScheme();
+
+ List parentOptionsList = createCassandraSynopsisOptionsText(parentCommandOptions(commandSpec));
+ List commandOptionsList = createCassandraSynopsisOptionsText(commandSpec.options());
+
+ Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(commandSpec, colorScheme);
+ Ansi.Text endOfOptionsText = positionalParamText.plainString().isEmpty() ?
+ colorScheme.text("") :
+ colorScheme.text("[")
+ .concat(commandSpec.parser().endOfOptionsDelimiter())
+ .concat("]");
+ Ansi.Text commandText = createCassandraSynopsisCommandText();
+
+ int width = commandSpec.usageMessage().width();
+ boolean isEmptyParent = commandSpec.parent() == null;
+ Ansi.Text mainCommandText = isEmptyParent ? colorScheme.commandText(commandSpec.name()) :
+ colorScheme.commandText(commandSpec.parent().qualifiedName());
+ TextTable textTable = TextTable.forColumns(colorScheme, new Column(width, columnIndent, Column.Overflow.WRAP));
+ textTable.indentWrappedLines = COLUMN_INDENT;
+ textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters());
+
+ // List
+ new LineBreakingLayout(colorScheme, width, textTable)
+ .concatItem(synopsisPrefix.isEmpty() ? mainCommandText : colorScheme.text(synopsisPrefix).concat(" ").concat(mainCommandText))
+ .concatItems(parentOptionsList)
+ .concatItem(isEmptyParent ? colorScheme.text("") : colorScheme.text(commandSpec.name()))
+ .concatItems(commandOptionsList)
+ .concatItem(endOfOptionsText)
+ // All other fields added to the synopsis are left-adjusted, so we don't need to add them one by one.
+ .flush(positionalParamText.concat(commandText));
+
+ textTable.addEmptyRow();
+ return textTable.toString();
+ }
+
+ private static Ansi.Text createCassandraSynopsisPositionalsText(CommandLine.Model.CommandSpec spec,
+ ColorScheme colorScheme)
+ {
+ List positionals = cassandraPositionals(spec);
+
+ Pair commandArgumensSpec = findBackwardCompatibleArgument(spec.userObject());
+ Ansi.Text text = colorScheme.text("");
+ // If the command has a backward compatible argument, use it to generate the synopsis based on the old format.
+ if (commandArgumensSpec != null)
+ return colorScheme.parameterText(commandArgumensSpec.left);
+
+ IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create();
+ for (CommandLine.Model.PositionalParamSpec positionalParam : positionals)
+ {
+ Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles());
+ text = text.plainString().isEmpty() ? label : text.concat(" ").concat(label);
+ }
+ return text;
+ }
+
+ private static List parentCommandOptions(CommandLine.Model.CommandSpec commandSpec)
+ {
+ List hierarhy = new LinkedList<>();
+ CommandLine.Model.CommandSpec curr;
+ while ((curr = commandSpec.parent()) != null)
+ {
+ hierarhy.add(curr);
+ commandSpec = curr;
+ }
+ Collections.reverse(hierarhy);
+ List options = new ArrayList<>();
+ for (CommandLine.Model.CommandSpec spec : hierarhy)
+ options.addAll(spec.options());
+ return options;
+ }
+
+ private List createCassandraSynopsisOptionsText(List options)
+ {
+ // Cassandra uses alphabetical order for options, ordered by short name.
+ List optionList = new ArrayList<>(options);
+ optionList.sort(createShortOptionNameComparator());
+ List result = new ArrayList<>();
+
+ ColorScheme colorScheme = colorScheme();
+ IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create();
+
+ for (CommandLine.Model.OptionSpec option : optionList)
+ {
+ if (option.hidden())
+ continue;
+
+ Ansi.Text text = ansi().new Text(0);
+ String[] names = sortShortestFirst(option.names());
+ if (names.length == 1)
+ {
+ text = text.concat("[").concat(colorScheme.optionText(names[0]))
+ .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme))
+ .concat("]");
+ }
+ else
+ {
+ Ansi.Text shortName = colorScheme.optionText(option.shortestName());
+ Ansi.Text fullName = colorScheme.optionText(option.longestName());
+ text = text.concat("[(")
+ .concat(shortName)
+ .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme))
+ .concat(" | ")
+ .concat(fullName)
+ .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme))
+ .concat(")]");
+ }
+
+ result.add(text);
+ }
+ return result;
+ }
+
+ @Override
+ public String optionListHeading(Object... params)
+ {
+ return createHeading(OPTIONS_HEADING, params);
+ }
+
+ @Override
+ public String optionList()
+ {
+ Comparator comparator = createShortOptionNameComparator();
+ List options = new LinkedList<>(parentCommandOptions(commandSpec()));
+ options.addAll(commandSpec().options());
+ options.sort(comparator);
+
+ Layout layout = cassandraSingleColumnOptionsParametersLayout();
+ layout.addAllOptions(options, CassandraStyleParamLabelRender.create());
+ return layout.toString();
+ }
+
+ @Override
+ public String endOfOptionsList()
+ {
+ List positionals = cassandraPositionals(commandSpec());
+ if (positionals.isEmpty())
+ return "";
+ Layout layout = cassandraSingleColumnOptionsParametersLayout();
+ layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, CassandraStyleParamLabelRender.create());
+ return layout.toString();
+ }
+
+ private Layout cassandraSingleColumnOptionsParametersLayout()
+ {
+ return new Layout(colorScheme(), configureLayoutTextTable(), new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer());
+ }
+
+ private TextTable configureLayoutTextTable()
+ {
+ TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width() - COLUMN_INDENT,
+ COLUMN_INDENT, Column.Overflow.WRAP));
+ table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters());
+ table.indentWrappedLines = DESCRIPTION_INDENT;
+ return table;
+ }
+
+ @Override
+ public String parameterList()
+ {
+ Pair cassandraArgument = findBackwardCompatibleArgument(commandSpec().userObject());
+ List positionalParams = cassandraPositionals(commandSpec());
+ TextTable table = configureLayoutTextTable();
+ Layout layout = cassandraArgument == null ?
+ cassandraSingleColumnOptionsParametersLayout() :
+ new Layout(colorScheme(),
+ table,
+ new CassandraStyleOptionRenderer(),
+ new CassandraStyleParameterRenderer())
+ {
+ // If the command has a backward compatible argument, use it to generate the synopsis
+ // based on the old format.
+ @Override
+ public void layout(CommandLine.Model.ArgSpec argSpec, Ansi.Text[][] cellValues)
+ {
+ Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), colorScheme);
+ cellValues[0] = new Ansi.Text[]{ colorScheme.parameterText(cassandraArgument.left) };
+ cellValues[1] = new Ansi.Text[]{ descPadding.concat(colorScheme.parameterText(cassandraArgument.right)) };
+ cellValues[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", colorScheme) };
+ for (Ansi.Text[] oneRow : cellValues)
+ table.addRowValues(oneRow);
+ }
+
+ @Override
+ public void addAllPositionalParameters(List params,
+ IParamLabelRenderer paramLabelRenderer)
+ {
+ layout(null, new Ansi.Text[3][]);
+ }
+ };
+
+ layout.addAllPositionalParameters(positionalParams, CassandraStyleParamLabelRender.create());
+ table.addEmptyRow();
+ return layout.toString();
+ }
+
+ @Override
+ public String commandList(Map subcommands)
+ {
+ if (subcommands.isEmpty())
+ return "";
+ int width = commandSpec().usageMessage().width();
+ int commandLength = Math.min(CommandUtils.maxLength(subcommands.keySet()), width / 2);
+ int leadinColumnWidth = commandLength + SUBCOMMANDS_INDENT;
+ CommandLine.Help.TextTable table = TextTable.forColumns(colorScheme(),
+ new CommandLine.Help.Column(leadinColumnWidth, SUBCOMMANDS_INDENT,
+ CommandLine.Help.Column.Overflow.SPAN),
+ new CommandLine.Help.Column(width - leadinColumnWidth, SUBCOMMANDS_INDENT,
+ CommandLine.Help.Column.Overflow.WRAP));
+ table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters());
+
+ for (Map.Entry entry : subcommands.entrySet())
+ {
+ CommandLine.Help help = entry.getValue();
+ CommandLine.Model.UsageMessageSpec usage = help.commandSpec().usageMessage();
+ String header = isEmpty(usage.header()) ? (isEmpty(usage.description()) ? "" : usage.description()[0]) : usage.header()[0];
+ Ansi.Text[] lines = colorScheme().text(header).splitLines();
+ for (int i = 0; i < lines.length; i++)
+ table.addRowValues(i == 0 ? help.commandNamesText(", ") : Ansi.OFF.new Text(0), lines[i]);
+ }
+ return table.toString();
+ }
+
+ @Override
+ public String footerHeading(Object... params)
+ {
+ return createHeading(FOOTER_HEADING, params);
+ }
+
+ @Override
+ public String footer(Object... params)
+ {
+ String[] footer;
+ if (commandSpec().parent() == null)
+ footer = isEmpty(commandSpec().usageMessage().footer()) ? new String[]{ USAGE_HELP_FOOTER } :
+ commandSpec().usageMessage().footer();
+ else
+ footer = EMPTY_FOOTER;
+
+ TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width(), 0, Column.Overflow.WRAP));
+ table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters());
+ table.indentWrappedLines = 0;
+
+ for (String summaryLine : footer)
+ table.addRowValues(String.format(summaryLine, params));
+ table.addEmptyRow();
+ return table.toString();
+ }
+
+ public String topLevelCommandListHeading(Object... params)
+ {
+ return createHeading(TOP_LEVEL_COMMAND_HEADING + "%n", params);
+ }
+
+ public String topLevelSynopsis(Object... params)
+ {
+ return printDetailedSynopsis(TOP_LEVEL_SYNOPSIS_LIST_PREFIX, 0, false);
+ }
+
+ private static List cassandraPositionals(CommandLine.Model.CommandSpec commandSpec)
+ {
+ List positionals = new ArrayList<>(commandSpec.positionalParameters());
+ positionals.removeIf(CommandLine.Model.ArgSpec::hidden);
+ return positionals;
+ }
+
+ /**
+ * Layout for cassandra help CLI output.
+ * @return List of keys for the help sections.
+ */
+ public static List cassandraHelpSectionKeys()
+ {
+ List result = new LinkedList<>();
+ result.add(SECTION_KEY_HEADER_HEADING);
+ result.add(SECTION_KEY_HEADER);
+ result.add(SECTION_KEY_DESCRIPTION_HEADING);
+ result.add(SECTION_KEY_DESCRIPTION);
+ result.add(SECTION_KEY_SYNOPSIS_HEADING);
+ result.add(SECTION_KEY_SYNOPSIS);
+ result.add(SECTION_KEY_OPTION_LIST_HEADING);
+ result.add(SECTION_KEY_OPTION_LIST);
+ result.add(SECTION_KEY_END_OF_OPTIONS);
+ result.add(SECTION_KEY_PARAMETER_LIST_HEADING);
+ result.add(SECTION_KEY_PARAMETER_LIST);
+ result.add(SECTION_KEY_COMMAND_LIST_HEADING);
+ result.add(SECTION_KEY_COMMAND_LIST);
+ result.add(SECTION_KEY_EXIT_CODE_LIST_HEADING);
+ result.add(SECTION_KEY_EXIT_CODE_LIST);
+ result.add(SECTION_KEY_FOOTER_HEADING);
+ result.add(SECTION_KEY_FOOTER);
+ return result;
+ }
+
+ private static Ansi.Text spacedParamLabel(CommandLine.Model.OptionSpec optionSpec,
+ IParamLabelRenderer parameterLabelRenderer,
+ ColorScheme scheme)
+ {
+ return optionSpec.typeInfo().isBoolean() ? scheme.text("") :
+ scheme.text(" ").concat(parameterLabelRenderer.renderParameterLabel(optionSpec, scheme.ansi(), scheme.optionParamStyles()));
+ }
+
+ private static class CassandraStyleParamLabelRender implements IParamLabelRenderer
+ {
+ public static IParamLabelRenderer create()
+ {
+ return new CassandraStyleParamLabelRender();
+ }
+
+ @Override
+ public Ansi.Text renderParameterLabel(CommandLine.Model.ArgSpec argSpec, Ansi ansi, List styles)
+ {
+ ColorScheme colorScheme = CommandLine.Help.defaultColorScheme(ansi);
+ if (argSpec.equals(CASSANDRA_END_OF_OPTIONS_OPTION))
+ return colorScheme.text("");
+ if (argSpec instanceof CommandLine.Model.OptionSpec && argSpec.typeInfo().isBoolean())
+ return colorScheme.text("");
+ return argSpec.isOption() ? colorScheme.optionText(argSpec.paramLabel()) :
+ colorScheme.parameterText(argSpec.paramLabel());
+ }
+
+ @Override
+ public String separator() { return ""; }
+ }
+
+ private static class CassandraStyleOptionRenderer implements IOptionRenderer
+ {
+ public Ansi.Text[][] render(CommandLine.Model.OptionSpec option, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme)
+ {
+ Ansi.Text optionText = scheme.optionText("");
+ for (int i = 0; i < option.names().length; i++)
+ {
+ String name = option.names()[i];
+ optionText = optionText.concat(scheme.optionText(name))
+ .concat(spacedParamLabel(option, parameterLabelRenderer, scheme))
+ .concat(i == option.names().length - 1 ? "" : ", ");
+ }
+
+ Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme);
+ Ansi.Text desc = scheme.optionText(option.description().length == 0 ? "" : option.description()[0]);
+
+ Ansi.Text[][] result = new Ansi.Text[3][];
+ result[0] = new Ansi.Text[]{ optionText };
+ result[1] = new Ansi.Text[]{ descPadding.concat(desc) };
+ result[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", scheme) };
+ return result;
+ }
+ }
+
+ private static class CassandraStyleParameterRenderer implements IParameterRenderer
+ {
+ @Override
+ public Ansi.Text[][] render(CommandLine.Model.PositionalParamSpec param, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme)
+ {
+ String descriptionString = param.description()[0];
+ Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), scheme);
+ Ansi.Text[][] result = new Ansi.Text[3][];
+ result[0] = new Ansi.Text[]{ parameterLabelRenderer.renderParameterLabel(param, scheme.ansi(), scheme.parameterStyles()) };
+ result[1] = new Ansi.Text[]{ descPadding.concat(scheme.parameterText(descriptionString)) };
+ result[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", scheme) };
+ return result;
+ }
+ }
+
+ private static class LineBreakingLayout
+ {
+ private static final int spaceWidth = 1;
+ private final int width;
+ private final TextTable textTable;
+ private final Ansi.Text padding;
+ /** Current line being built, always less than width. */
+ private Ansi.Text current;
+
+ public LineBreakingLayout(ColorScheme colorScheme, int width, TextTable textTable)
+ {
+ this.width = width - textTable.columns()[0].indent;
+ this.padding = colorScheme.text(leadingSpaces(textTable.indentWrappedLines));
+ this.textTable = textTable;
+ current = colorScheme.text("");
+ }
+
+ public LineBreakingLayout concatItems(List items)
+ {
+ for (Ansi.Text item : items)
+ concatItem(item);
+ return this;
+ }
+
+ public LineBreakingLayout concatItem(Ansi.Text item)
+ {
+ if (item.plainString().isEmpty())
+ return this;
+
+ if (current.plainString().length() + spaceWidth + item.plainString().length() >= width)
+ {
+ textTable.addRowValues(current);
+ current = padding.concat(item);
+ }
+ else
+ current = current == padding || current.plainString().isEmpty() ?
+ current.concat(item) :
+ current.concat(" ").concat(item);
+ return this;
+ }
+
+ public void flush(Ansi.Text end)
+ {
+ textTable.addRowValues(current == padding ? end : current.concat(" ").concat(end));
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/management/CommandUtils.java b/src/java/org/apache/cassandra/management/CommandUtils.java
new file mode 100644
index 000000000000..5363c941b298
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/CommandUtils.java
@@ -0,0 +1,73 @@
+/*
+ * 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.management;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+
+import org.apache.cassandra.utils.Pair;
+
+/**
+ * Utility methods for nodetool commands.
+ */
+public final class CommandUtils
+{
+ /**
+ * Returns a string with the given number of leading spaces.
+ *
+ * @param num the number of leading spaces
+ * @return the string with the given number of leading spaces
+ */
+ public static String leadingSpaces(int num)
+ {
+ char[] buff = new char[num];
+ Arrays.fill(buff, ' ');
+ return new String(buff);
+ }
+
+ public static int maxLength(Collection> any)
+ {
+ int result = 0;
+ for (Object value : any)
+ result = Math.max(result, String.valueOf(value).length());
+ return result;
+ }
+
+ public static Pair findBackwardCompatibleArgument(Object userObject)
+ {
+ Class> clazz = userObject.getClass();
+ for (Field field : clazz.getFields())
+ {
+ if (field.isAnnotationPresent(ParameterUsage.class))
+ {
+ ParameterUsage ann = field.getAnnotation(ParameterUsage.class);
+ return Pair.create(ann.usage(), ann.description());
+ }
+ }
+ return null;
+ }
+
+ public static String[] sortShortestFirst(String[] names)
+ {
+ Arrays.sort(names, Comparator.comparing(String::length));
+ return names;
+ }
+}
diff --git a/src/java/org/apache/cassandra/management/ParameterUsage.java b/src/java/org/apache/cassandra/management/ParameterUsage.java
new file mode 100644
index 000000000000..1f541dfe58f1
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/ParameterUsage.java
@@ -0,0 +1,36 @@
+/*
+ * 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.management;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Argument annotation for Cassandra commands, used to provide a message
+ * for command-line argument for backward compatibility when help is requested.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD })
+public @interface ParameterUsage
+{
+ String description() default "";
+ String usage() default "";
+}
diff --git a/src/java/org/apache/cassandra/management/ServiceMBeanBridge.java b/src/java/org/apache/cassandra/management/ServiceMBeanBridge.java
new file mode 100644
index 000000000000..d37cbaf8d4df
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/ServiceMBeanBridge.java
@@ -0,0 +1,41 @@
+/*
+ * 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.management;
+
+import org.apache.cassandra.db.compaction.CompactionManagerMBean;
+import org.apache.cassandra.service.StorageServiceMBean;
+
+/**
+ * Service bridge for Cassandra management commands to access MBeans and other service-related functionality.
+ * This interface is used to bridge the gap between the nodetool commands and the Cassandra service MBeans.
+ */
+public interface ServiceMBeanBridge
+{
+ T getMBean(Class clazz);
+
+ default StorageServiceMBean ssProxy()
+ {
+ return getMBean(StorageServiceMBean.class);
+ }
+
+ default CompactionManagerMBean cmProxy()
+ {
+ return getMBean(CompactionManagerMBean.class);
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java
similarity index 61%
rename from src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java
rename to src/java/org/apache/cassandra/management/api/AbortBootstrap.java
index 4c180ad81cff..a9c1f7ddec27 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/AbortBootstrap.java
+++ b/src/java/org/apache/cassandra/management/api/AbortBootstrap.java
@@ -15,34 +15,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.tools.nodetool;
+package org.apache.cassandra.management.api;
-
-import io.airlift.airline.Command;
-import io.airlift.airline.Option;
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.StringUtils.isEmpty;
@Command(name = "abortbootstrap", description = "Abort a failed bootstrap")
-public class AbortBootstrap extends NodeToolCmd
+public class AbortBootstrap extends BaseCommand
{
- @Option(title = "node id", name = "--node", description = "Node ID of the node that failed bootstrap", required = false)
- private String nodeId = EMPTY;
-
- @Option(title = "ip", name = "--ip", description = "IP of the node that failed bootstrap", required = false)
- private String endpoint = EMPTY;
+ @Option(names = "--node", description = "Node ID of the node that failed bootstrap")
+ public String nodeId = EMPTY;
+ @Option(names = "--ip", description = "IP of the node that failed bootstrap")
+ public String ip = EMPTY;
@Override
- public void execute(NodeProbe probe)
+ public void execute(ServiceMBeanBridge probe)
{
- if (isEmpty(nodeId) && isEmpty(endpoint))
+ if (isEmpty(nodeId) && isEmpty(ip))
throw new IllegalArgumentException("Either --node or --ip needs to be set");
- if (!isEmpty(nodeId) && !isEmpty(endpoint))
+ if (!isEmpty(nodeId) && !isEmpty(ip))
throw new IllegalArgumentException("Only one of --node or --ip need to be set");
- probe.abortBootstrap(nodeId, endpoint);
+ probe.ssProxy().abortBootstrap(nodeId, ip);
}
}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Assassinate.java b/src/java/org/apache/cassandra/management/api/Assassinate.java
similarity index 51%
rename from src/java/org/apache/cassandra/tools/nodetool/Assassinate.java
rename to src/java/org/apache/cassandra/management/api/Assassinate.java
index 2639ec81b32f..1da182a0ca10 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Assassinate.java
+++ b/src/java/org/apache/cassandra/management/api/Assassinate.java
@@ -15,33 +15,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.tools.nodetool;
+package org.apache.cassandra.management.api;
-import static org.apache.commons.lang3.StringUtils.EMPTY;
-import io.airlift.airline.Arguments;
-import io.airlift.airline.Command;
-
-import java.net.UnknownHostException;
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import picocli.CommandLine;
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
-@Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode")
-public class Assassinate extends NodeToolCmd
+@CommandLine.Command(name = "assassinate", description = "Forcefully remove a dead node without re-replicating any data. Use as a last resort if you cannot removenode")
+public class Assassinate extends BaseCommand
{
- @Arguments(title = "ip address", usage = "", description = "IP address of the endpoint to assassinate", required = true)
- private String endpoint = EMPTY;
+ @CommandLine.Parameters(description = "IP address of the endpoint to assassinate", arity = "1")
+ public String ipAddress = EMPTY;
@Override
- public void execute(NodeProbe probe)
+ public void execute(ServiceMBeanBridge probe)
{
- try
- {
- probe.assassinateEndpoint(endpoint);
- }
- catch (UnknownHostException e)
- {
- throw new RuntimeException(e);
- }
+ probe.ssProxy().assassinateEndpoint(ipAddress);
}
}
diff --git a/src/java/org/apache/cassandra/management/api/Compact.java b/src/java/org/apache/cassandra/management/api/Compact.java
new file mode 100644
index 000000000000..078c3f7b8fba
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/api/Compact.java
@@ -0,0 +1,115 @@
+/*
+ * 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.management.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ParameterUsage;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import picocli.CommandLine;
+
+import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalKeyspace;
+import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parseOptionalTables;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+
+@CommandLine.Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables")
+public class Compact extends BaseCommand
+{
+ @ParameterUsage(usage = "[...] or ...",
+ description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined")
+ @CommandLine.Parameters(index = "0..*", description = "The keyspace followed by one or many tables or " +
+ "list of SSTable data files when using --user-defined")
+ public List args = new ArrayList<>();
+
+ @CommandLine.Option(names = { "-s", "--split-output" }, description = "Use -s to not create a single big file")
+ public boolean splitOutput = false;
+
+ @CommandLine.Option(names = { "--user-defined" },
+ description = "Use --user-defined to submit listed files for user-defined compaction")
+ public boolean userDefined = false;
+
+ @CommandLine.Option(names = { "-st", "--start-token" },
+ description = "Use -st to specify a token at which the compaction range starts (inclusive)")
+ public String startToken = EMPTY;
+
+ @CommandLine.Option(names = { "-et", "--end-token" },
+ description = "Use -et to specify a token at which compaction range ends (inclusive)")
+ public String endToken = EMPTY;
+
+ @CommandLine.Option(names = { "--partition" },
+ description = "String representation of the partition key")
+ public String partitionKey = EMPTY;
+
+ @Override
+ public void execute(ServiceMBeanBridge probe)
+ {
+ final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty());
+ final boolean partitionKeyProvided = !partitionKey.isEmpty();
+ final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided;
+ if (splitOutput && (userDefined || tokenProvided))
+ {
+ throw new RuntimeException("Invalid option combination: Can not use split-output here");
+ }
+ if (userDefined && tokenProvided)
+ {
+ throw new RuntimeException("Invalid option combination: Can not provide tokens when using user-defined");
+ }
+
+ if (userDefined)
+ {
+ try
+ {
+ String userDefinedFiles = String.join(",", args);
+ probe.cmProxy().forceUserDefinedCompaction(userDefinedFiles);
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Error occurred during user defined compaction", e);
+ }
+ return;
+ }
+
+ List keyspaces = parseOptionalKeyspace(args, probe);
+ String[] tableNames = parseOptionalTables(args);
+
+ for (String keyspace : keyspaces)
+ {
+ try
+ {
+ if (startEndTokenProvided)
+ {
+ probe.ssProxy().forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames);
+ }
+ else if (partitionKeyProvided)
+ {
+ probe.ssProxy().forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames);
+ }
+ else
+ {
+ probe.ssProxy().forceKeyspaceCompaction(splitOutput, keyspace, tableNames);
+ }
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Error occurred during compaction", e);
+ }
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java b/src/java/org/apache/cassandra/management/api/ForceCompact.java
similarity index 53%
rename from src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java
rename to src/java/org/apache/cassandra/management/api/ForceCompact.java
index 99265e7bf0fa..cba5148f3e89 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/ForceCompact.java
+++ b/src/java/org/apache/cassandra/management/api/ForceCompact.java
@@ -16,28 +16,40 @@
* limitations under the License.
*/
-package org.apache.cassandra.tools.nodetool;
+package org.apache.cassandra.management.api;
-import io.airlift.airline.Arguments;
-import io.airlift.airline.Command;
-
-import java.util.ArrayList;
import java.util.List;
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import com.google.common.collect.Lists;
+
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ParameterUsage;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import picocli.CommandLine;
import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.parsePartitionKeys;
-@Command(name = "forcecompact", description = "Force a (major) compaction on a table")
-public class ForceCompact extends NodeToolCmd
+@CommandLine.Command(name = "forcecompact", description = "Force a (major) compaction on a table")
+public class ForceCompact extends BaseCommand
{
- @Arguments(usage = "[
]", description = "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds")
- private List args = new ArrayList<>();
+ @ParameterUsage(usage = "[
]",
+ description = "The keyspace, table, and a list of partition keys ignoring the gc_grace_seconds")
+ public List args;
+
+ @CommandLine.Parameters(index = "0", arity = "1", description = "The keyspace name to compact")
+ public String keyspace;
+
+ @CommandLine.Parameters(index = "1", arity = "1", description = "The table name to compact")
+ public String table;
+
+ @CommandLine.Parameters(index = "2..*", arity = "1", description = "The partition keys to compact")
+ public String[] keys;
@Override
- public void execute(NodeProbe probe)
+ public void execute(ServiceMBeanBridge probe)
{
+ args = Lists.asList(keyspace, table, keys);
// Check if the input has valid size
checkArgument(args.size() >= 3, "forcecompact requires keyspace, table and keys args");
@@ -48,11 +60,11 @@ public void execute(NodeProbe probe)
try
{
- probe.forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace);
+ probe.ssProxy().forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace);
}
catch (Exception e)
{
throw new RuntimeException("Error occurred during compaction keys", e);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/java/org/apache/cassandra/management/api/JmxConnect.java b/src/java/org/apache/cassandra/management/api/JmxConnect.java
new file mode 100644
index 000000000000..142e418eb6d8
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/api/JmxConnect.java
@@ -0,0 +1,175 @@
+/*
+ * 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.management.api;
+
+import java.io.IOException;
+import javax.inject.Inject;
+
+import com.google.common.base.Throwables;
+
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import org.apache.cassandra.tools.INodeProbeFactory;
+import picocli.CommandLine;
+
+import static java.lang.Integer.parseInt;
+import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.promptAndReadPassword;
+import static org.apache.cassandra.tools.NodeTool.NodeToolCmd.readUserPasswordFromFile;
+import static org.apache.cassandra.tools.NodeToolV2.lastExecutableSubcommandWithSameParent;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
+
+/**
+ * Command options for NodeTool commands that are executed via JMX.
+ */
+@CommandLine.Command(name = "connect", description = "Connect to a Cassandra node via JMX")
+public class JmxConnect extends BaseCommand implements AutoCloseable
+{
+ public static final String MIXIN_KEY = "jmx";
+
+ /** The command specification, used to access command-specific properties. */
+ @CommandLine.Spec
+ protected CommandLine.Model.CommandSpec spec; // injected by picocli
+
+ @CommandLine.Option(names = { "-h", "--host" }, description = "Node hostname or ip address")
+ public String host = "127.0.0.1";
+
+ @CommandLine.Option(names = { "-p", "--port" }, description = "Remote jmx agent port number")
+ public String port = "7199";
+
+ @CommandLine.Option(names = { "-u", "--username" }, description = "Remote jmx agent username")
+ public String username = EMPTY;
+
+ @CommandLine.Option(names = { "-pw", "--password" }, description = "Remote jmx agent password")
+ public String password = EMPTY;
+
+ @CommandLine.Option(names = { "-pwf", "--password-file" }, description = "Path to the JMX password file")
+ public String passwordFilePath = EMPTY;
+
+ @CommandLine.Option(names = { "-pp", "--print-port" }, description = "Operate in 4.0 mode with hosts disambiguated by port number")
+ public boolean printPort = false;
+
+ @Inject
+ private INodeProbeFactory nodeProbeFactory;
+
+ /**
+ * This method is called by picocli and used depending on the execution strategy.
+ * @param parseResult The parsed command line.
+ * @return The exit code.
+ */
+ public static int executionStrategy(CommandLine.ParseResult parseResult)
+ {
+ CommandLine.Model.CommandSpec jmx = parseResult.commandSpec().mixins().get(MIXIN_KEY);
+ if (jmx == null)
+ throw new CommandLine.InitializationException("No JmxConnect command found in the top-level hierarchy");
+
+ try (JmxConnectionCommandInvoker invoker = new JmxConnectionCommandInvoker((JmxConnect) jmx.userObject()))
+ {
+ return invoker.execute(parseResult);
+ }
+ catch (JmxConnectionCommandInvoker.CloseException e)
+ {
+ jmx.commandLine()
+ .getErr()
+ .println("Failed to connect to JMX: " + e.getMessage());
+ return jmx.commandLine().getExitCodeExceptionMapper().getExitCode(e);
+ }
+ }
+
+ /**
+ * Initialize the JMX connection to the Cassandra node using the provided options.
+ */
+ @Override
+ protected void execute(ServiceMBeanBridge probe)
+ {
+ assert probe == null;
+ try
+ {
+ if (isNotEmpty(username)) {
+ if (isNotEmpty(passwordFilePath))
+ password = readUserPasswordFromFile(username, passwordFilePath);
+
+ if (isEmpty(password))
+ password = promptAndReadPassword();
+ }
+
+ bridge = username.isEmpty() ? nodeProbeFactory.create(host, parseInt(port))
+ : nodeProbeFactory.create(host, parseInt(port), username, password);
+ }
+ catch (IOException | SecurityException e)
+ {
+ Throwable rootCause = Throwables.getRootCause(e);
+ logger.error("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port,
+ rootCause.getClass().getSimpleName(), rootCause.getMessage());
+ throw new CommandLine.ExecutionException(spec.commandLine(), "Failed to connect to JMX", e);
+ }
+ }
+
+ @Override
+ public void close() throws Exception
+ {
+ if (bridge instanceof AutoCloseable)
+ ((AutoCloseable) bridge).close();
+ }
+
+ private static class JmxConnectionCommandInvoker implements CommandLine.IExecutionStrategy, AutoCloseable
+ {
+ private final JmxConnect connect;
+
+ public JmxConnectionCommandInvoker(JmxConnect connect)
+ {
+ this.connect = connect;
+ }
+
+ @Override
+ public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
+ {
+ CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
+ if (lastParent.userObject() instanceof BaseCommand)
+ {
+ connect.run();
+ ((BaseCommand) lastParent.userObject()).setBridge(connect.getBridge());
+ }
+ return new CommandLine.RunLast().execute(parseResult);
+ }
+
+ @Override
+ public void close() throws CloseException
+ {
+ try
+ {
+ if (connect.getBridge() instanceof AutoCloseable)
+ ((AutoCloseable) connect.getBridge()).close();
+ }
+ catch (Exception e)
+ {
+ throw new CloseException("Failed to close JMX connection", e);
+ }
+ }
+
+ private static class CloseException extends RuntimeException
+ {
+ public CloseException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/management/api/TopLevelCommand.java b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java
new file mode 100644
index 000000000000..54eb6b628a5c
--- /dev/null
+++ b/src/java/org/apache/cassandra/management/api/TopLevelCommand.java
@@ -0,0 +1,45 @@
+/*
+ * 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.management.api;
+
+import org.apache.cassandra.management.CassandraHelpCommand;
+import picocli.CommandLine;
+
+import static org.apache.cassandra.management.CassandraHelpCommand.printTopCommandUsage;
+
+@CommandLine.Command(name = "nodetool",
+ description = "Manage your Cassandra cluster",
+ subcommands = { CassandraHelpCommand.class,
+ AbortBootstrap.class,
+ Assassinate.class,
+ ForceCompact.class,
+ Compact.class,
+ Version.class})
+public class TopLevelCommand implements Runnable
+{
+ @CommandLine.Spec
+ public CommandLine.Model.CommandSpec spec;
+
+ public void run()
+ {
+ printTopCommandUsage(spec.commandLine(),
+ spec.commandLine().getColorScheme(),
+ spec.commandLine().getOut());
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Version.java b/src/java/org/apache/cassandra/management/api/Version.java
similarity index 56%
rename from src/java/org/apache/cassandra/tools/nodetool/Version.java
rename to src/java/org/apache/cassandra/management/api/Version.java
index 6556a046272f..fc8d24a009bc 100644
--- a/src/java/org/apache/cassandra/tools/nodetool/Version.java
+++ b/src/java/org/apache/cassandra/management/api/Version.java
@@ -15,26 +15,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.tools.nodetool;
+package org.apache.cassandra.management.api;
-import io.airlift.airline.Command;
-import io.airlift.airline.Option;
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
+import org.apache.cassandra.management.BaseCommand;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import picocli.CommandLine;
+import picocli.CommandLine.Option;
-@Command(name = "version", description = "Print cassandra version")
-public class Version extends NodeToolCmd
+@CommandLine.Command(name = "version", description = "Print cassandra version")
+public class Version extends BaseCommand
{
- @Option(title = "verbose",
- name = {"-v", "--verbose"},
- description = "Include additional information")
+ @Option(names = {"-v", "--verbose"}, description = "Include addtitional information")
private boolean verbose = false;
@Override
- public void execute(NodeProbe probe)
+ protected void execute(ServiceMBeanBridge probe)
{
- probe.output().out.println("ReleaseVersion: " + probe.getReleaseVersion());
- if (verbose)
- probe.output().out.println("GitSHA: " + probe.getGitSHA());
+ logger.out.println("ReleaseVersion: " + probe.ssProxy().getReleaseVersion());
+ if (verbose) {
+ logger.out.println("GitSHA: " + probe.ssProxy().getGitSHA());
+ }
+
}
}
diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java
index b121cb310091..65c21a5f0b8b 100644
--- a/src/java/org/apache/cassandra/tools/NodeProbe.java
+++ b/src/java/org/apache/cassandra/tools/NodeProbe.java
@@ -37,11 +37,12 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-
+import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
@@ -55,12 +56,20 @@
import javax.management.remote.JMXServiceURL;
import javax.rmi.ssl.SslRMIClientSocketFactory;
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Uninterruptibles;
+
import org.apache.cassandra.audit.AuditLogManager;
import org.apache.cassandra.audit.AuditLogManagerMBean;
import org.apache.cassandra.audit.AuditLogOptions;
import org.apache.cassandra.audit.AuditLogOptionsCompositeData;
-
-import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.auth.AuthCache;
import org.apache.cassandra.auth.AuthCacheMBean;
import org.apache.cassandra.auth.CIDRGroupsMappingManager;
@@ -97,12 +106,12 @@
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.metrics.ThreadPoolMetrics;
+import org.apache.cassandra.management.ServiceMBeanBridge;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.MessagingServiceMBean;
import org.apache.cassandra.service.ActiveRepairServiceMBean;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.CacheServiceMBean;
-import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.service.GCInspector;
import org.apache.cassandra.service.GCInspectorMXBean;
import org.apache.cassandra.service.StorageProxy;
@@ -111,19 +120,10 @@
import org.apache.cassandra.streaming.StreamManagerMBean;
import org.apache.cassandra.streaming.StreamState;
import org.apache.cassandra.streaming.management.StreamStateCompositeData;
-import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
-
-import com.google.common.base.Function;
-import com.google.common.base.Strings;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.Uninterruptibles;
-
import org.apache.cassandra.tcm.CMSOperations;
+import org.apache.cassandra.tcm.CMSOperationsMBean;
import org.apache.cassandra.tools.nodetool.GetTimeout;
+import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
import org.apache.cassandra.utils.NativeLibrary;
import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
@@ -132,7 +132,7 @@
/**
* JMX client operations for Cassandra.
*/
-public class NodeProbe implements AutoCloseable
+public class NodeProbe implements AutoCloseable, ServiceMBeanBridge
{
private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
private static final String ssObjName = "org.apache.cassandra.db:type=StorageService";
@@ -175,6 +175,7 @@ public class NodeProbe implements AutoCloseable
private boolean failed;
protected CIDRFilteringMetricsTableMBean cfmProxy;
+ private final Map cachedMBeans = new HashMap<>();
/**
* Creates a NodeProbe using the specified JMX host, port, username, and password.
@@ -233,6 +234,24 @@ protected NodeProbe()
this.output = Output.CONSOLE;
}
+ private static T cachedNewMBeanProxy(BiConsumer cache,
+ MBeanServerConnection connection,
+ ObjectName objectName,
+ Class clazz)
+ {
+
+ T proxy = JMX.newMBeanProxy(connection, objectName, clazz);
+ cache.accept(clazz.getName(), proxy);
+ return proxy;
+ }
+
+ @Override
+ public T getMBean(Class clazz)
+ {
+ return clazz.cast(Optional.ofNullable(cachedMBeans.get(clazz.getName()))
+ .orElseThrow(() -> new IllegalArgumentException("No MBean found for " + clazz.getName())));
+ }
+
/**
* Create a connection to the JMX agent and setup the M[X]Bean proxies.
*
@@ -262,55 +281,49 @@ protected void connect() throws IOException
try
{
ObjectName name = new ObjectName(ssObjName);
- ssProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageServiceMBean.class);
+ ssProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StorageServiceMBean.class);
name = new ObjectName(CMSOperations.MBEAN_OBJECT_NAME);
- cmsProxy = JMX.newMBeanProxy(mbeanServerConn, name, CMSOperationsMBean.class);
+ cmsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CMSOperationsMBean.class);
name = new ObjectName(MessagingService.MBEAN_NAME);
- msProxy = JMX.newMBeanProxy(mbeanServerConn, name, MessagingServiceMBean.class);
+ msProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, MessagingServiceMBean.class);
name = new ObjectName(StreamManagerMBean.OBJECT_NAME);
- streamProxy = JMX.newMBeanProxy(mbeanServerConn, name, StreamManagerMBean.class);
+ streamProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StreamManagerMBean.class);
name = new ObjectName(CompactionManager.MBEAN_OBJECT_NAME);
- compactionProxy = JMX.newMBeanProxy(mbeanServerConn, name, CompactionManagerMBean.class);
+ compactionProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CompactionManagerMBean.class);
name = new ObjectName(FailureDetector.MBEAN_NAME);
- fdProxy = JMX.newMBeanProxy(mbeanServerConn, name, FailureDetectorMBean.class);
+ fdProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, FailureDetectorMBean.class);
name = new ObjectName(CacheService.MBEAN_NAME);
- cacheService = JMX.newMBeanProxy(mbeanServerConn, name, CacheServiceMBean.class);
+ cacheService = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CacheServiceMBean.class);
name = new ObjectName(StorageProxy.MBEAN_NAME);
- spProxy = JMX.newMBeanProxy(mbeanServerConn, name, StorageProxyMBean.class);
+ spProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, StorageProxyMBean.class);
name = new ObjectName(HintsService.MBEAN_NAME);
- hsProxy = JMX.newMBeanProxy(mbeanServerConn, name, HintsServiceMBean.class);
+ hsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, HintsServiceMBean.class);
name = new ObjectName(GCInspector.MBEAN_NAME);
- gcProxy = JMX.newMBeanProxy(mbeanServerConn, name, GCInspectorMXBean.class);
+ gcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, GCInspectorMXBean.class);
name = new ObjectName(Gossiper.MBEAN_NAME);
- gossProxy = JMX.newMBeanProxy(mbeanServerConn, name, GossiperMBean.class);
+ gossProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, GossiperMBean.class);
name = new ObjectName(BatchlogManager.MBEAN_NAME);
- bmProxy = JMX.newMBeanProxy(mbeanServerConn, name, BatchlogManagerMBean.class);
+ bmProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, BatchlogManagerMBean.class);
name = new ObjectName(ActiveRepairServiceMBean.MBEAN_NAME);
- arsProxy = JMX.newMBeanProxy(mbeanServerConn, name, ActiveRepairServiceMBean.class);
+ arsProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, ActiveRepairServiceMBean.class);
name = new ObjectName(AuditLogManager.MBEAN_NAME);
- almProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuditLogManagerMBean.class);
+ almProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, AuditLogManagerMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME);
- ccProxy = JMX.newMBeanProxy(mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class);
+ ccProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, PasswordAuthenticator.CredentialsCacheMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME);
- jpcProxy = JMX.newMBeanProxy(mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class);
-
+ jpcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, AuthorizationProxy.JmxPermissionsCacheMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME);
- npcProxy = JMX.newMBeanProxy(mbeanServerConn, name, NetworkPermissionsCacheMBean.class);
-
+ npcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, NetworkPermissionsCacheMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME);
- pcProxy = JMX.newMBeanProxy(mbeanServerConn, name, PermissionsCacheMBean.class);
-
+ pcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, PermissionsCacheMBean.class);
name = new ObjectName(AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME);
- rcProxy = JMX.newMBeanProxy(mbeanServerConn, name, RolesCacheMBean.class);
-
+ rcProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, RolesCacheMBean.class);
name = new ObjectName(CIDRPermissionsManager.MBEAN_NAME);
- cpbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRPermissionsManagerMBean.class);
-
+ cpbProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRPermissionsManagerMBean.class);
name = new ObjectName(CIDRGroupsMappingManager.MBEAN_NAME);
- cmbProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class);
-
+ cmbProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRGroupsMappingManagerMBean.class);
name = new ObjectName(CIDRFilteringMetricsTable.MBEAN_NAME);
- cfmProxy = JMX.newMBeanProxy(mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class);
+ cfmProxy = cachedNewMBeanProxy(cachedMBeans::put, mbeanServerConn, name, CIDRFilteringMetricsTableMBean.class);
}
catch (MalformedObjectNameException e)
{
diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java
index bd1e302ba066..8df3a1625b8c 100644
--- a/src/java/org/apache/cassandra/tools/NodeTool.java
+++ b/src/java/org/apache/cassandra/tools/NodeTool.java
@@ -17,20 +17,7 @@
*/
package org.apache.cassandra.tools;
-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 org.apache.cassandra.io.util.File.WriteMode.APPEND;
-import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY;
-import static org.apache.commons.lang3.StringUtils.EMPTY;
-import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.apache.commons.lang3.StringUtils.isNotEmpty;
-
import java.io.Console;
-import org.apache.cassandra.io.util.File;
-import org.apache.cassandra.io.util.FileWriter;
import java.io.FileNotFoundException;
import java.io.IOError;
import java.io.IOException;
@@ -44,16 +31,13 @@
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.SortedMap;
-
+import java.util.TreeMap;
+import java.util.function.Consumer;
import javax.management.InstanceNotFoundException;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
-
-import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
-import org.apache.cassandra.tools.nodetool.*;
-import org.apache.cassandra.utils.FBUtilities;
-
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import io.airlift.airline.Cli;
@@ -67,6 +51,33 @@
import io.airlift.airline.ParseOptionConversionException;
import io.airlift.airline.ParseOptionMissingException;
import io.airlift.airline.ParseOptionMissingValueException;
+import io.airlift.airline.UsageHelper;
+import io.airlift.airline.UsagePrinter;
+import io.airlift.airline.model.CommandGroupMetadata;
+import io.airlift.airline.model.CommandMetadata;
+import io.airlift.airline.model.GlobalMetadata;
+import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.io.util.FileWriter;
+import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
+import org.apache.cassandra.management.CassandraHelpLayout;
+import org.apache.cassandra.management.ServiceMBeanBridge;
+import org.apache.cassandra.tools.nodetool.*;
+import org.apache.cassandra.utils.FBUtilities;
+import picocli.CommandLine;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Throwables.getStackTraceAsString;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+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.stream.Collectors.toList;
+import static org.apache.cassandra.io.util.File.WriteMode.APPEND;
+import static org.apache.commons.lang3.ArrayUtils.EMPTY_STRING_ARRAY;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
public class NodeTool
{
@@ -75,6 +86,7 @@ public class NodeTool
FBUtilities.preventIllegalAccessWarnings();
}
+ private static final int FALLBACK_NODETOOL_V1 = -100;
private static final String HISTORYFILE = "nodetool.history";
private final INodeProbeFactory nodeProbeFactory;
@@ -94,14 +106,11 @@ public NodeTool(INodeProbeFactory nodeProbeFactory, Output output)
public int execute(String... args)
{
List> commands = newArrayList(
- AbortBootstrap.class,
- Assassinate.class,
CassHelp.class,
CIDRFilteringStats.class,
Cleanup.class,
ClearSnapshot.class,
ClientStats.class,
- Compact.class,
CompactionHistory.class,
CompactionStats.class,
DataPaths.class,
@@ -232,9 +241,7 @@ public int execute(String... args)
UpdateCIDRGroup.class,
UpgradeSSTable.class,
Verify.class,
- Version.class,
- ViewBuildStatus.class,
- ForceCompact.class
+ ViewBuildStatus.class
);
Cli.CliBuilder builder = Cli.builder("nodetool");
@@ -272,9 +279,28 @@ public int execute(String... args)
int status = 0;
try
{
- NodeToolCmdRunnable parse = parser.parse(args);
- printHistory(args);
- parse.run(nodeProbeFactory, output);
+ // Try to run the command with the new parser, if it у fails, fallback to the old parser.
+ int result = new NodeToolV2(nodeProbeFactory, output)
+ // Filter out the help command, and nodetool command, and fallback to the old help message.
+ .withCommandNameFilter(cmd -> cmd.equals("help") || cmd.equals("nodetool"), FALLBACK_NODETOOL_V1)
+ .withParameterExceptionHandler((ex, arg) -> {
+ if (ex instanceof CommandLine.UnmatchedArgumentException)
+ return FALLBACK_NODETOOL_V1;
+ badUse(ex);
+ return 1;
+ })
+ .withExecutionExceptionHandler((ex, c, arg) -> {
+ err(ex);
+ return 2;
+ }).execute(args);
+
+ if (result >= 0)
+ return result;
+
+ // Fallback to the old parser, and run the command.
+ assert result == FALLBACK_NODETOOL_V1;
+ NodeToolCmdRunnable cmd = parser.parse(args);
+ cmd.run(nodeProbeFactory, output);
} catch (IllegalArgumentException |
IllegalStateException |
ParseArgumentsMissingException |
@@ -296,7 +322,7 @@ public int execute(String... args)
return status;
}
- private static void printHistory(String... args)
+ public 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)
@@ -316,32 +342,96 @@ private static void printHistory(String... args)
}
}
+ public static void badUse(Consumer out, Throwable e)
+ {
+ out.accept("nodetool: " + e.getMessage());
+ out.accept("See 'nodetool help' or 'nodetool help '.");
+ }
+
protected void badUse(Exception e)
{
- output.out.println("nodetool: " + e.getMessage());
- output.out.println("See 'nodetool help' or 'nodetool help '.");
+ badUse(output.out::println, e);
}
- protected void err(Throwable e)
+ public static void err(Consumer out, Throwable e)
{
// CASSANDRA-11537: friendly error message when server is not ready
if (e instanceof InstanceNotFoundException)
throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool.");
- output.err.println("error: " + e.getMessage());
- output.err.println("-- StackTrace --");
- output.err.println(getStackTraceAsString(e));
+ out.accept("error: " + e.getMessage());
+ out.accept("-- StackTrace --");
+ out.accept(getStackTraceAsString(e));
+ }
+
+ protected void err(Throwable e)
+ {
+ err(output.err::println, e);
}
public static class CassHelp extends Help implements NodeToolCmdRunnable
{
public void run(INodeProbeFactory nodeProbeFactory, Output output)
{
- run();
+ StringBuilder sb = new StringBuilder();
+ NodeToolV2 cmd = new NodeToolV2(nodeProbeFactory, output);
+ if (command.isEmpty())
+ {
+ usage(global, cmd.getCommandsDescription(), sb);
+ }
+ else
+ {
+ if (cmd.isCommandPresent(command.get(0)))
+ cmd.execute("help", command.get(0));
+ else
+ help(global, command, sb);
+ }
+
+ output.out.println(sb);
+ }
+
+ public static void usage(GlobalMetadata global, Map extraCommands, StringBuilder sb)
+ {
+ UsagePrinter out = new UsagePrinter(sb, CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH);
+ List commandArguments = global.getOptions().stream()
+ .filter(option -> !option.isHidden())
+ .map(UsageHelper::toUsage)
+ .collect(toImmutableList());
+
+ out.newPrinterWithHangingIndent(CassandraHelpLayout.COLUMN_INDENT)
+ .append(CassandraHelpLayout.TOP_LEVEL_SYNOPSIS_LIST_PREFIX)
+ .append(global.getName())
+ .appendWords(commandArguments)
+ .append(CassandraHelpLayout.SYNOPSIS_SUBCOMMANDS_LABEL)
+ .newline()
+ .newline();
+
+ Map commands = getCommandsDescription(global);
+ // Remove the help command from the list of extra commands if exists, as it's not applicable for backward compatibility.
+ extraCommands.remove("help");
+ commands.putAll(extraCommands);
+
+ out.append(CassandraHelpLayout.TOP_LEVEL_COMMAND_HEADING).newline();
+ out.newIndentedPrinter(CassandraHelpLayout.SUBCOMMANDS_INDENT)
+ .appendTable(commands.entrySet().stream()
+ .map(entry -> ImmutableList.of(entry.getKey(), firstNonNull(entry.getValue(), "")))
+ .collect(toList()));
+ out.newline();
+ out.append(CassandraHelpLayout.USAGE_HELP_FOOTER);
+ }
+
+ private static Map getCommandsDescription(GlobalMetadata global)
+ {
+ Map commands = new TreeMap<>();
+ for (CommandMetadata meta : global.getDefaultGroupCommands())
+ commands.put(meta.getName(), meta.getDescription());
+ for (CommandGroupMetadata grp : global.getCommandGroups())
+ commands.put(grp.getName(), grp.getDescription());
+ return commands;
}
}
- interface NodeToolCmdRunnable
+ public interface NodeToolCmdRunnable
{
void run(INodeProbeFactory nodeProbeFactory, Output output);
}
@@ -401,7 +491,7 @@ public void runInternal()
}
- private String readUserPasswordFromFile(String username, String passwordFilePath) {
+ public static String readUserPasswordFromFile(String username, String passwordFilePath) {
String password = EMPTY;
File passwordFile = new File(passwordFilePath);
@@ -429,7 +519,7 @@ private String readUserPasswordFromFile(String username, String passwordFilePath
return password;
}
- private String promptAndReadPassword()
+ public static String promptAndReadPassword()
{
String password = EMPTY;
@@ -469,12 +559,12 @@ protected enum KeyspaceSet
ALL, NON_SYSTEM, NON_LOCAL_STRATEGY
}
- protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe)
+ public static List parseOptionalKeyspace(List cmdArgs, ServiceMBeanBridge nodeProbe)
{
return parseOptionalKeyspace(cmdArgs, nodeProbe, KeyspaceSet.ALL);
}
- protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nodeProbe, KeyspaceSet defaultKeyspaceSet)
+ public static List parseOptionalKeyspace(List cmdArgs, ServiceMBeanBridge nodeProbe, KeyspaceSet defaultKeyspaceSet)
{
List keyspaces = new ArrayList<>();
@@ -482,11 +572,11 @@ protected List parseOptionalKeyspace(List cmdArgs, NodeProbe nod
if (cmdArgs == null || cmdArgs.isEmpty())
{
if (defaultKeyspaceSet == KeyspaceSet.NON_LOCAL_STRATEGY)
- keyspaces.addAll(keyspaces = nodeProbe.getNonLocalStrategyKeyspaces());
+ keyspaces.addAll(keyspaces = nodeProbe.ssProxy().getNonLocalStrategyKeyspaces());
else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM)
- keyspaces.addAll(keyspaces = nodeProbe.getNonSystemKeyspaces());
+ keyspaces.addAll(keyspaces = nodeProbe.ssProxy().getNonSystemKeyspaces());
else
- keyspaces.addAll(nodeProbe.getKeyspaces());
+ keyspaces.addAll(nodeProbe.ssProxy().getKeyspaces());
}
else
{
@@ -495,19 +585,19 @@ else if (defaultKeyspaceSet == KeyspaceSet.NON_SYSTEM)
for (String keyspace : keyspaces)
{
- if (!nodeProbe.getKeyspaces().contains(keyspace))
+ if (!nodeProbe.ssProxy().getKeyspaces().contains(keyspace))
throw new IllegalArgumentException("Keyspace [" + keyspace + "] does not exist.");
}
return Collections.unmodifiableList(keyspaces);
}
- protected String[] parseOptionalTables(List cmdArgs)
+ public static String[] parseOptionalTables(List cmdArgs)
{
return cmdArgs.size() <= 1 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(1, cmdArgs.size()), String.class);
}
- protected String[] parsePartitionKeys(List cmdArgs)
+ public static String[] parsePartitionKeys(List cmdArgs)
{
return cmdArgs.size() <= 2 ? EMPTY_STRING_ARRAY : toArray(cmdArgs.subList(2, cmdArgs.size()), String.class);
}
diff --git a/src/java/org/apache/cassandra/tools/NodeToolV2.java b/src/java/org/apache/cassandra/tools/NodeToolV2.java
new file mode 100644
index 000000000000..a19404729e4c
--- /dev/null
+++ b/src/java/org/apache/cassandra/tools/NodeToolV2.java
@@ -0,0 +1,225 @@
+/*
+ * 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.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+import javax.inject.Inject;
+
+import com.google.common.base.Throwables;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.management.CassandraHelpLayout;
+import org.apache.cassandra.management.api.JmxConnect;
+import org.apache.cassandra.management.api.TopLevelCommand;
+import org.apache.cassandra.utils.FBUtilities;
+import picocli.CommandLine;
+
+import static org.apache.cassandra.tools.NodeTool.badUse;
+import static org.apache.cassandra.tools.NodeTool.err;
+import static org.apache.cassandra.tools.NodeTool.printHistory;
+
+public class NodeToolV2
+{
+ static
+ {
+ FBUtilities.preventIllegalAccessWarnings();
+ }
+
+ private final INodeProbeFactory nodeProbeFactory;
+ private final Output output;
+ private CommandLine.IParameterExceptionHandler parameterExceptionHandler = (ex, arg) -> {
+ badUse(ex.getCommandLine().getOut()::println, Throwables.getRootCause(ex));
+ return 1;
+ };
+
+ private CommandLine.IExecutionExceptionHandler executionExceptionHandler = (ex, cmdLine, parseResult) -> {
+ err(cmdLine.getErr()::println, Throwables.getRootCause(ex));
+ return 2;
+ };
+
+ private CommandLine.IExecutionStrategy strategy;
+
+ public static void main(String... args)
+ {
+ System.exit(new NodeToolV2(new NodeProbeFactory(), Output.CONSOLE).execute(args));
+ }
+
+ public NodeToolV2(INodeProbeFactory nodeProbeFactory, Output output)
+ {
+ this.nodeProbeFactory = nodeProbeFactory;
+ this.output = output;
+ }
+
+ /**
+ * Execute the command line utility with the given arguments via the JMX connection.
+ * @param args command line arguments
+ * @return 0 on success, 1 on bad use, 2 on execution error
+ */
+ public int execute(String... args)
+ {
+ return execute(createCommandLine(new CassandraCliFactory(nodeProbeFactory, output)), args);
+ }
+
+ protected int execute(CommandLine commandLine, String... args)
+ {
+ try
+ {
+ configureCliLayout(commandLine);
+ commandLine.setExecutionStrategy(strategy == null ? JmxConnect::executionStrategy : strategy)
+ .setExecutionExceptionHandler(executionExceptionHandler)
+ .setParameterExceptionHandler(parameterExceptionHandler);
+
+ printHistory(args);
+ return commandLine.execute(args);
+ }
+ catch (Exception e)
+ {
+ err(output.err::println, e);
+ return 2;
+ }
+ }
+
+ /**
+ * Filter the command by its name and return the given exit code when the filter is matched.
+ * @param commandPredicate the predicate to filter the command name.
+ * @param exitCodeWhenMatched the exit code to return when the filter is matched.
+ * @return this instance.
+ */
+ public NodeToolV2 withCommandNameFilter(Predicate commandPredicate, int exitCodeWhenMatched)
+ {
+ strategy = parsed -> {
+ CommandLine.Model.CommandSpec spec = lastExecutableSubcommandWithSameParent(parsed.asCommandLineList());
+ if (commandPredicate.test(spec.name()))
+ return exitCodeWhenMatched;
+ return JmxConnect.executionStrategy(parsed);
+ };
+ return this;
+ }
+
+ public NodeToolV2 withParameterExceptionHandler(CommandLine.IParameterExceptionHandler handler)
+ {
+ parameterExceptionHandler = handler;
+ return this;
+ }
+
+ public NodeToolV2 withExecutionExceptionHandler(CommandLine.IExecutionExceptionHandler handler)
+ {
+ executionExceptionHandler = handler;
+ return this;
+ }
+
+ public boolean isCommandPresent(String commandName)
+ {
+ CommandLine commandLine = createCommandLine(new CassandraCliFactory(nodeProbeFactory, output));
+ return commandLine.getSubcommands().values().stream()
+ .anyMatch(sub -> sub.getCommandName().equals(commandName));
+ }
+
+ public Map getCommandsDescription()
+ {
+ Map commands = new TreeMap<>();
+ CommandLine commandLine = createCommandLine(new CassandraCliFactory(nodeProbeFactory, output));
+ commandLine.getSubcommands()
+ .values()
+ .forEach(sub -> commands.put(sub.getCommandName(),
+ CommandLine.Help.join(commandLine.getColorScheme().ansi(), 1000,
+ sub.getCommandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters(),
+ sub.getCommandSpec().usageMessage().description(), new StringBuilder()).toString()));
+ // Remove the help command from the list of commands, as it's not applicable for backward compatibility.
+ return commands;
+ }
+
+ public static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List parsedCommands)
+ {
+ int start = parsedCommands.size() - 1;
+ for (int i = parsedCommands.size() - 2; i >= 0; i--)
+ {
+ if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent())
+ break;
+ start = i;
+ }
+ return parsedCommands.get(start).getCommandSpec();
+ }
+
+ private static CommandLine createCommandLine(CassandraCliFactory factory)
+ {
+ return new CommandLine(new TopLevelCommand(), factory)
+ .addMixin(JmxConnect.MIXIN_KEY, factory.create(JmxConnect.class))
+ .setOut(new PrintWriter(factory.output.out, true))
+ .setErr(new PrintWriter(factory.output.err, true));
+ }
+
+ private static void configureCliLayout(CommandLine commandLine)
+ {
+ if (CassandraRelevantProperties.CASSANDRA_CLI_PICOCLI_LAYOUT.getBoolean())
+ return;
+
+ commandLine.setHelpFactory(CassandraHelpLayout::new)
+ .setUsageHelpWidth(CassandraHelpLayout.DEFAULT_USAGE_HELP_WIDTH)
+ .setHelpSectionKeys(CassandraHelpLayout.cassandraHelpSectionKeys());
+ }
+
+ private static class CassandraCliFactory implements CommandLine.IFactory
+ {
+ private final CommandLine.IFactory fallback;
+ private final INodeProbeFactory nodeProbeFactory;
+ private final Output output;
+
+ public CassandraCliFactory(INodeProbeFactory nodeProbeFactory, Output output)
+ {
+ this.fallback = CommandLine.defaultFactory();
+ this.nodeProbeFactory = nodeProbeFactory;
+ this.output = output;
+ }
+
+ @Override
+ public K create(Class cls)
+ {
+ try
+ {
+ K bean = this.fallback.create(cls);
+ Class> beanClass = bean.getClass();
+ do
+ {
+ Field[] fields = beanClass.getDeclaredFields();
+ for (Field field : fields)
+ {
+ if (!field.isAnnotationPresent(Inject.class))
+ continue;
+ field.setAccessible(true);
+ if (field.getType().equals(INodeProbeFactory.class))
+ field.set(bean, nodeProbeFactory);
+ else if (field.getType().equals(Output.class))
+ field.set(bean, output);
+ }
+ }
+ while ((beanClass = beanClass.getSuperclass()) != null);
+ return bean;
+ }
+ catch (Exception e)
+ {
+ throw new CommandLine.InitializationException("Failed to create instance of " + cls, e);
+ }
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/tools/Output.java b/src/java/org/apache/cassandra/tools/Output.java
index 1d2fcb3a6325..db0b22fad652 100644
--- a/src/java/org/apache/cassandra/tools/Output.java
+++ b/src/java/org/apache/cassandra/tools/Output.java
@@ -32,4 +32,14 @@ public Output(PrintStream out, PrintStream err)
this.out = out;
this.err = err;
}
+
+ public void info(String msg, Object... args)
+ {
+ out.printf(msg, args);
+ }
+
+ public void error(String msg, Object... args)
+ {
+ err.printf(msg, args);
+ }
}
diff --git a/src/java/org/apache/cassandra/tools/nodetool/Compact.java b/src/java/org/apache/cassandra/tools/nodetool/Compact.java
deleted file mode 100644
index f5a83ed90475..000000000000
--- a/src/java/org/apache/cassandra/tools/nodetool/Compact.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.nodetool;
-
-import static org.apache.commons.lang3.StringUtils.EMPTY;
-
-import io.airlift.airline.Arguments;
-import io.airlift.airline.Command;
-import io.airlift.airline.Option;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.cassandra.tools.NodeProbe;
-import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
-
-@Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables")
-public class Compact extends NodeToolCmd
-{
- @Arguments(usage = "[...] or ...", description = "The keyspace followed by one or many tables or list of SSTable data files when using --user-defined")
- private List args = new ArrayList<>();
-
- @Option(title = "split_output", name = {"-s", "--split-output"}, description = "Use -s to not create a single big file")
- private boolean splitOutput = false;
-
- @Option(title = "user-defined", name = {"--user-defined"}, description = "Use --user-defined to submit listed files for user-defined compaction")
- private boolean userDefined = false;
-
- @Option(title = "start_token", name = {"-st", "--start-token"}, description = "Use -st to specify a token at which the compaction range starts (inclusive)")
- private String startToken = EMPTY;
-
- @Option(title = "end_token", name = {"-et", "--end-token"}, description = "Use -et to specify a token at which compaction range ends (inclusive)")
- private String endToken = EMPTY;
-
- @Option(title = "partition_key", name = {"--partition"}, description = "String representation of the partition key")
- private String partitionKey = EMPTY;
-
-
- @Override
- public void execute(NodeProbe probe)
- {
- final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty());
- final boolean partitionKeyProvided = !partitionKey.isEmpty();
- final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided;
- if (splitOutput && (userDefined || tokenProvided))
- {
- throw new RuntimeException("Invalid option combination: Can not use split-output here");
- }
- if (userDefined && tokenProvided)
- {
- throw new RuntimeException("Invalid option combination: Can not provide tokens when using user-defined");
- }
-
- if (userDefined)
- {
- try
- {
- String userDefinedFiles = String.join(",", args);
- probe.forceUserDefinedCompaction(userDefinedFiles);
- } catch (Exception e) {
- throw new RuntimeException("Error occurred during user defined compaction", e);
- }
- return;
- }
-
- List keyspaces = parseOptionalKeyspace(args, probe);
- String[] tableNames = parseOptionalTables(args);
-
- for (String keyspace : keyspaces)
- {
- try
- {
- if (startEndTokenProvided)
- {
- probe.forceKeyspaceCompactionForTokenRange(keyspace, startToken, endToken, tableNames);
- }
- else if (partitionKeyProvided)
- {
- probe.forceKeyspaceCompactionForPartitionKey(keyspace, partitionKey, tableNames);
- }
- else
- {
- probe.forceKeyspaceCompaction(splitOutput, keyspace, tableNames);
- }
- } catch (Exception e)
- {
- throw new RuntimeException("Error occurred during compaction", e);
- }
- }
- }
-}
diff --git a/test/resources/nodetool/help/abortbootstrap b/test/resources/nodetool/help/abortbootstrap
new file mode 100644
index 000000000000..c6493ba31d82
--- /dev/null
+++ b/test/resources/nodetool/help/abortbootstrap
@@ -0,0 +1,34 @@
+NAME
+ nodetool abortbootstrap - Abort a failed bootstrap
+
+SYNOPSIS
+ nodetool [(-h | --host )] [(-p | --port )]
+ [(-pp | --print-port)] [(-pw | --password )]
+ [(-pwf | --password-file )]
+ [(-u | --username )] abortbootstrap [--ip ]
+ [--node ]
+
+OPTIONS
+ -h , --host
+ Node hostname or ip address
+
+ --ip
+ IP of the node that failed bootstrap
+
+ --node
+ Node ID of the node that failed bootstrap
+
+ -p , --port
+ Remote jmx agent port number
+
+ -pp, --print-port
+ Operate in 4.0 mode with hosts disambiguated by port number
+
+ -pw , --password
+ Remote jmx agent password
+
+ -pwf , --password-file
+ Path to the JMX password file
+
+ -u , --username
+ Remote jmx agent username
\ No newline at end of file
diff --git a/test/resources/nodetool/help/assassinate b/test/resources/nodetool/help/assassinate
new file mode 100644
index 000000000000..c8c34e401154
--- /dev/null
+++ b/test/resources/nodetool/help/assassinate
@@ -0,0 +1,38 @@
+NAME
+ nodetool assassinate - Forcefully remove a dead node without
+ re-replicating any data. Use as a last resort if you cannot
+ removenode
+
+SYNOPSIS
+ nodetool [(-h | --host )] [(-p | --port )]
+ [(-pp | --print-port)] [(-pw | --password )]
+ [(-pwf | --password-file )]
+ [(-u | --username )] assassinate [--]
+
+
+OPTIONS
+ -h , --host
+ Node hostname or ip address
+
+ -p , --port
+ Remote jmx agent port number
+
+ -pp, --print-port
+ Operate in 4.0 mode with hosts disambiguated by port number
+
+ -pw , --password
+ Remote jmx agent password
+
+ -pwf , --password-file
+ Path to the JMX password file
+
+ -u , --username
+ Remote jmx agent username
+
+ --
+ This option can be used to separate command-line options from
+ the list of argument, (useful when arguments might be mistaken
+ for command-line options
+
+
+ IP address of the endpoint to assassinate
\ No newline at end of file
diff --git a/test/resources/nodetool/help/compact b/test/resources/nodetool/help/compact
new file mode 100644
index 000000000000..a9ca45ccfba7
--- /dev/null
+++ b/test/resources/nodetool/help/compact
@@ -0,0 +1,59 @@
+NAME
+ nodetool compact - Force a (major) compaction on one or more tables
+ or user-defined compaction on given SSTables
+
+SYNOPSIS
+ nodetool [(-h | --host )] [(-p | --port )]
+ [(-pp | --print-port)] [(-pw | --password )]
+ [(-pwf | --password-file )]
+ [(-u | --username )] compact
+ [(-et | --end-token )]
+ [--partition ] [(-s | --split-output)]
+ [(-st | --start-token )] [--user-defined]
+ [--] [...] or ...
+
+OPTIONS
+ -et , --end-token
+ Use -et to specify a token at which compaction range ends
+ (inclusive)
+
+ -h , --host
+ Node hostname or ip address
+
+ -p , --port
+ Remote jmx agent port number
+
+ --partition
+ String representation of the partition key
+
+ -pp, --print-port
+ Operate in 4.0 mode with hosts disambiguated by port number
+
+ -pw , --password
+ Remote jmx agent password
+
+ -pwf , --password-file
+ Path to the JMX password file
+
+ -s, --split-output
+ Use -s to not create a single big file
+
+ -st , --start-token
+ Use -st to specify a token at which the compaction range starts
+ (inclusive)
+
+ -u , --username
+ Remote jmx agent username
+
+ --user-defined
+ Use --user-defined to submit listed files for user-defined
+ compaction
+
+ --
+ This option can be used to separate command-line options from
+ the list of argument, (useful when arguments might be mistaken
+ for command-line options
+
+ [...] or ...
+ The keyspace followed by one or many tables or list of SSTable
+ data files when using --user-defined
\ No newline at end of file
diff --git a/test/resources/nodetool/help/forcecompact b/test/resources/nodetool/help/forcecompact
new file mode 100644
index 000000000000..b248f0ac88f6
--- /dev/null
+++ b/test/resources/nodetool/help/forcecompact
@@ -0,0 +1,37 @@
+NAME
+ nodetool forcecompact - Force a (major) compaction on a table
+
+SYNOPSIS
+ nodetool [(-h | --host )] [(-p | --port )]
+ [(-pp | --print-port)] [(-pw | --password )]
+ [(-pwf | --password-file )]
+ [(-u | --username )] forcecompact [--]
+ [
]
+
+OPTIONS
+ -h , --host
+ Node hostname or ip address
+
+ -p , --port
+ Remote jmx agent port number
+
+ -pp, --print-port
+ Operate in 4.0 mode with hosts disambiguated by port number
+
+ -pw , --password
+ Remote jmx agent password
+
+ -pwf , --password-file
+ Path to the JMX password file
+
+ -u , --username
+ Remote jmx agent username
+
+ --
+ This option can be used to separate command-line options from
+ the list of argument, (useful when arguments might be mistaken
+ for command-line options
+
+ [
]
+ The keyspace, table, and a list of partition keys ignoring the
+ gc_grace_seconds
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/tools/ToolRunner.java b/test/unit/org/apache/cassandra/tools/ToolRunner.java
index ad1f80e8127d..fd5042e0d243 100644
--- a/test/unit/org/apache/cassandra/tools/ToolRunner.java
+++ b/test/unit/org/apache/cassandra/tools/ToolRunner.java
@@ -32,6 +32,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -48,7 +50,9 @@
import org.apache.cassandra.utils.Pair;
import org.assertj.core.util.Lists;
+import static com.github.jknack.handlebars.internal.lang3.ArrayUtils.isEmpty;
import static org.assertj.core.api.Assertions.assertThat;
+
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -191,6 +195,16 @@ public static ToolResult invokeNodetool(List args)
return invoke(CQLTester.buildNodetoolArgs(args));
}
+ public static ToolResult invokeNodetool(Map env, String... args)
+ {
+ return invokeNodetool(env, Arrays.asList(args));
+ }
+
+ public static ToolResult invokeNodetool(Map env, List args)
+ {
+ return invoke(env, CQLTester.buildNodetoolArgs(args));
+ }
+
public static ToolResult invoke(List args)
{
return invoke(args.toArray(new String[args.size()]));
@@ -305,6 +319,27 @@ public NodeToolResult get()
res.right.getException());
}
+ public static ToolRunner.ToolResult invokeNodetoolInJvm(BiFunction factory, String... commands)
+ {
+ LinesOutputStream out = new LinesOutputStream(logger::info);
+ LinesOutputStream err = new LinesOutputStream(logger::error);
+ List args = CQLTester.buildNodetoolArgs(isEmpty(commands) ? new ArrayList<>() : List.of(commands));
+ args.remove("bin/nodetool");
+ try
+ {
+ Object runner = factory.apply(new NodeProbeFactory(), new Output(new PrintStream(out), new PrintStream(err)));
+ Object result = runner.getClass().getMethod("execute", String[].class)
+ .invoke(runner, new Object[] { args.toArray(new String[0]) });
+ assertTrue(result instanceof Integer);
+ return new ToolResult(args, (Integer) result, out.getOutput(), err.getOutput(), null);
+ }
+ catch (Exception e)
+ {
+ return new ToolResult(args, -1, out.getOutput(),
+ err.getOutput() + '\n' + Throwables.getStackTraceAsString(e), e);
+ }
+ }
+
public static Pair invokeSupplier(Supplier runMe)
{
return invokeSupplier(runMe, null);
@@ -528,6 +563,43 @@ public AssertHelp errorContainsAny(String... messages)
return this;
}
+ public AssertHelp outputContains(String messages)
+ {
+ return outputContainsAny(messages);
+ }
+
+ public AssertHelp outputContainsAny(String... messages)
+ {
+ assertThat(messages).hasSizeGreaterThan(0);
+ assertThat(stdout).isNotNull();
+ if (!Stream.of(messages).anyMatch(stdout::contains))
+ fail("stdout does not contain " + Arrays.toString(messages));
+ return this;
+ }
+
+ public AssertHelp outputMatches(String messages)
+ {
+ return outputMatchesAny(messages);
+ }
+
+ public AssertHelp outputMatchesAny(String... messages)
+ {
+ assertThat(messages).hasSizeGreaterThan(0);
+ assertThat(stdout).isNotNull();
+ if (!Stream.of(messages).anyMatch(stdout::matches))
+ fail("stdout does not contain " + Arrays.toString(messages));
+ return this;
+ }
+
+ public AssertHelp outputLinesMatchesAll(String... messages)
+ {
+ assertThat(messages).hasSizeGreaterThan(0);
+ assertThat(stdout).isNotNull();
+ if (!Stream.of(messages).allMatch((String msg) -> stdout.lines().anyMatch((String line) -> line.matches(msg))))
+ fail("stdout does not match " + Arrays.toString(messages));
+ return this;
+ }
+
private void fail(String msg)
{
StringBuilder sb = new StringBuilder();
@@ -739,4 +811,46 @@ public ToolResult invoke()
return ToolRunner.invoke(env, stdin, args);
}
}
+
+ private static class LinesOutputStream extends OutputStream
+ {
+ private final List outputLines = new ArrayList<>();
+ private final StringBuilder buffer = new StringBuilder();
+ private final Consumer logger;
+
+ public LinesOutputStream(Consumer logger)
+ {
+ this.logger = logger;
+ }
+
+ @Override
+ public void write(int b)
+ {
+ char c = (char) b;
+ if (c == '\n')
+ {
+ // Add the buffer to the list if it's a new line
+ outputLines.add(buffer.toString());
+ logger.accept(buffer.toString());
+ buffer.setLength(0); // Clear the buffer
+ }
+ else
+ buffer.append(c);
+ }
+
+ public void flush()
+ {
+ if (buffer.length() > 0)
+ {
+ outputLines.add(buffer.toString());
+ logger.accept(buffer.toString());
+ buffer.setLength(0);
+ }
+ }
+
+ public String getOutput()
+ {
+ return String.join("\n", outputLines);
+ }
+ }
}
diff --git a/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java
new file mode 100644
index 000000000000..1ee337042d45
--- /dev/null
+++ b/test/unit/org/apache/cassandra/tools/nodetool/CQLToolRunnerTester.java
@@ -0,0 +1,139 @@
+/*
+ * 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.nodetool;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import com.github.difflib.DiffUtils;
+import com.github.difflib.patch.AbstractDelta;
+import com.github.difflib.patch.Patch;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.NodeTool;
+import org.apache.cassandra.tools.NodeToolV2;
+import org.apache.cassandra.tools.ToolRunner;
+
+@RunWith(Parameterized.class)
+public abstract class CQLToolRunnerTester extends CQLTester
+{
+ public static final Map runnersMap = Map.of(
+ "invokeNodetoolV1InJvm", CQLToolRunnerTester::invokeNodetoolV1InJvm,
+ "invokeNodetoolV2InJvm", CQLToolRunnerTester::invokeNodetoolV2InJvm);
+
+ @Parameterized.Parameter
+ public String runner;
+
+ @Parameterized.Parameters(name = "runner={0}")
+ public static Collection