From 5c94bc88207908234b4332ba337a0fd65c5b0308 Mon Sep 17 00:00:00 2001 From: Yuqi Yan Date: Mon, 15 Apr 2024 12:08:22 -0700 Subject: [PATCH] Add nodetool get/setguardrailsconfig commands patch by Yuqi Yan; reviewed by Brandon Williams, Maxwell Guo, Stefan Miklosovic for CASSANDRA-19552 Co-authored-by: Stefan Miklosovic --- CHANGES.txt | 1 + .../org/apache/cassandra/tools/NodeProbe.java | 11 + .../org/apache/cassandra/tools/NodeTool.java | 4 + .../nodetool/GuardrailsConfigCommand.java | 455 ++++++++++++++++++ .../GuardrailsConfigCommandsTest.java | 342 +++++++++++++ 5 files changed, 813 insertions(+) create mode 100644 src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java diff --git a/CHANGES.txt b/CHANGES.txt index f0453dec97..20c6c89a98 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.1.10 + * Add nodetool get/setguardrailsconfig commands (CASSANDRA-19552) Merged from 4.0: * Ensure prepared_statement INSERT timestamp precedes eviction DELETE (CASSANDRA-19703) diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index b73f6dd818..969f6a6d09 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -75,6 +75,8 @@ import org.apache.cassandra.batchlog.BatchlogManagerMBean; import org.apache.cassandra.db.ColumnFamilyStoreMBean; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.CompactionManagerMBean; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.guardrails.GuardrailsMBean; import org.apache.cassandra.fql.FullQueryLoggerOptions; import org.apache.cassandra.fql.FullQueryLoggerOptionsCompositeData; import org.apache.cassandra.gms.FailureDetector; @@ -154,6 +156,7 @@ public class NodeProbe implements AutoCloseable protected NetworkPermissionsCacheMBean npcProxy; protected PermissionsCacheMBean pcProxy; protected RolesCacheMBean rcProxy; + protected GuardrailsMBean grProxy; protected Output output; private boolean failed; @@ -278,6 +281,9 @@ public class NodeProbe implements AutoCloseable pcProxy = JMX.newMBeanProxy(mbeanServerConn, name, PermissionsCacheMBean.class); name = new ObjectName(AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME); rcProxy = JMX.newMBeanProxy(mbeanServerConn, name, RolesCacheMBean.class); + + name = new ObjectName(Guardrails.MBEAN_NAME); + grProxy = JMX.newMBeanProxy(mbeanServerConn, name, GuardrailsMBean.class); } catch (MalformedObjectNameException e) { @@ -2146,6 +2152,11 @@ public class NodeProbe implements AutoCloseable { return ssProxy.getDefaultKeyspaceReplicationFactor(); } + + public GuardrailsMBean getGuardrailsMBean() + { + return grProxy; + } } class ColumnFamilyStoreMBeanIterator implements Iterator> diff --git a/src/java/org/apache/cassandra/tools/NodeTool.java b/src/java/org/apache/cassandra/tools/NodeTool.java index b54cbee738..3037e38531 100644 --- a/src/java/org/apache/cassandra/tools/NodeTool.java +++ b/src/java/org/apache/cassandra/tools/NodeTool.java @@ -50,6 +50,8 @@ import com.google.common.base.Throwables; import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.tools.nodetool.*; +import org.apache.cassandra.tools.nodetool.GuardrailsConfigCommand.GetGuardrailsConfig; +import org.apache.cassandra.tools.nodetool.GuardrailsConfigCommand.SetGuardrailsConfig; import org.apache.cassandra.utils.FBUtilities; import com.google.common.collect.Maps; @@ -142,6 +144,7 @@ public class NodeTool GetDefaultKeyspaceRF.class, GetEndpoints.class, GetFullQueryLog.class, + GetGuardrailsConfig.class, GetInterDCStreamThroughput.class, GetLoggingLevels.class, GetMaxHintWindow.class, @@ -200,6 +203,7 @@ public class NodeTool SetConcurrentCompactors.class, SetConcurrentViewBuilders.class, SetDefaultKeyspaceRF.class, + SetGuardrailsConfig.class, SetHintedHandoffThrottleInKB.class, SetInterDCStreamThroughput.class, SetLoggingLevel.class, diff --git a/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java b/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java new file mode 100644 index 0000000000..7e1d1e3400 --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommand.java @@ -0,0 +1,455 @@ +/* + * 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.io.PrintStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.annotations.VisibleForTesting; + +import io.airlift.airline.Arguments; +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import org.apache.cassandra.db.guardrails.GuardrailsMBean; +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.NodeTool; +import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; + +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +public abstract class GuardrailsConfigCommand extends NodeTool.NodeToolCmd +{ + @Command(name = "getguardrailsconfig", description = "Print runtime configuration of guardrails.") + public static class GetGuardrailsConfig extends GuardrailsConfigCommand + { + @Option(name = { "--category", "-c" }, + description = "Category of guardrails to filter, can be one of 'values', 'thresholds', 'flags', 'others'.", + allowedValues = { "values", "thresholds", "flags", "others" }) + private String guardrailCategory; + + @Arguments(description = "Specific names or guardrails to get configuration of.") + private List args = new ArrayList<>(); + + @Override + public void execute(NodeProbe probe) + { + GuardrailCategory categoryEnum = GuardrailCategory.parseCategory(guardrailCategory, probe.output().out); + + if (!args.isEmpty() && categoryEnum != null) + throw new IllegalStateException("Do not specify additional arguments when --category/-c is set."); + + List allGetters = stream(probe.getGuardrailsMBean().getClass().getDeclaredMethods()) + .filter(method -> method.getName().startsWith("get") + && !method.getName().endsWith("CSV")) + .filter(method -> args.isEmpty() || args.contains(toSnakeCase(method.getName().substring(3)))) + .sorted(comparing(Method::getName)) + .collect(toList()); + + display(probe, allGetters, categoryEnum); + } + + @Override + public void addRow(List bucket, GuardrailsMBean mBean, Method method, String guardrailName) throws Throwable + { + Class returnType = method.getReturnType(); + Object value = method.invoke(mBean); + + if (returnType.equals(int.class) || returnType.equals(Integer.class) + || returnType.equals(long.class) || returnType.equals(Long.class) + || returnType.equals(boolean.class) || returnType.equals(Boolean.class) + || returnType.equals(Set.class)) + { + constructRow(bucket, guardrailName, value.toString()); + } + else if (returnType.equals(String.class)) + { + if (value == null || value.toString().isEmpty()) + constructRow(bucket, guardrailName, "null"); + else + constructRow(bucket, guardrailName, value.toString()); + } + else + { + throw new RuntimeException("unhandled return type: " + returnType.getTypeName()); + } + } + } + + @Command(name = "setguardrailsconfig", description = "Modify runtime configuration of guardrails.") + public static class SetGuardrailsConfig extends GuardrailsConfigCommand + { + private static final Pattern SETTER_PATTERN = Pattern.compile("^set"); + + @Option(name = { "--list", "-l" }, + description = "List all available guardrails setters") + private boolean list; + + @Option(name = { "--category", "-c" }, + description = "Category of guardrails to filter, can be one of 'values', 'thresholds', 'flags', 'others'.", + allowedValues = { "values", "thresholds", "flags", "others" }) + private String guardrailCategory; + + @Arguments(usage = "[ ...]", + description = "For flags, possible values are 'true' or 'false'. " + + "For thresholds, two values are expected, first for warning, second for failure. " + + "For values, one value is expected, multiple values separated by comma.") + private final List args = new ArrayList<>(); + + @Override + public void execute(NodeProbe probe) + { + if (!list && guardrailCategory != null) + throw new IllegalStateException("--category/-c can be specified only together with --list/-l"); + + GuardrailCategory categoryEnum = GuardrailCategory.parseCategory(guardrailCategory, probe.output().out); + + if (args.isEmpty() && !list) + throw new IllegalStateException("No arguments."); + + if (list) + display(probe, getAllSetters(probe), categoryEnum); + else + executeSetter(probe); + } + + @Override + public void addRow(List bucket, GuardrailsMBean mBean, Method method, String guardrailName) throws Throwable + { + if (method.getParameterTypes().length == 1) + constructRow(bucket, sanitizeSetterName(method), method.getParameterTypes()[0].getName()); + else + constructRow(bucket, sanitizeSetterName(method), stream(method.getParameterTypes()).map(Class::getName).collect(toList()).toString()); + } + + private List getAllSetters(NodeProbe probe) + { + return stream(probe.getGuardrailsMBean().getClass().getDeclaredMethods()) + .filter(method -> method.getName().startsWith("set") && !method.getName().endsWith("CSV")) + .filter(method -> args.isEmpty() || args.contains(toSnakeCase(method.getName().substring(3)))) + .sorted(comparing(Method::getName)) + .collect(toList()); + } + + private String sanitizeSetterName(Method setter) + { + return toSnakeCase(SETTER_PATTERN.matcher(setter.getName()).replaceAll("")); + } + + private void executeSetter(NodeProbe nodeProbe) + { + String snakeCaseName = args.get(0); + String setterName = toCamelCase(args.get(0).startsWith("set_") ? args.get(0) : "set_" + args.get(0)); + + Method setter = getAllSetters(nodeProbe).stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException(format("Setter method %s not found. " + + "Run nodetool setguardrailsconfig --list " + + "to see available setters", setterName))); + + validateArguments(setter, snakeCaseName, args); + + List methodArgs = args.subList(1, args.size()); + try + { + setter.invoke(nodeProbe.getGuardrailsMBean(), prepareArguments(methodArgs, setter)); + } + catch (Exception ex) + { + String reason; + if (ex.getCause() != null && ex.getCause().getMessage() != null) + reason = ex.getCause().getMessage(); + else + reason = ex.getMessage(); + + throw new IllegalStateException(format("Error occured when setting the config for setter %s with arguments %s: %s", + snakeCaseName, methodArgs, reason)); + } + } + + private void validateArguments(Method setter, String setterName, List args) + { + if (args.size() != setter.getParameterCount() + 1) + { + throw new IllegalStateException(format("%s is expecting %d argument values. Getting %d instead.", + setterName, + setter.getParameterCount(), + args.size() - 1)); + } + } + + private Object[] prepareArguments(List args, Method method) + { + Class[] parameterTypes = method.getParameterTypes(); + Object[] arguments = new Object[args.size()]; + + for (int i = 0; i < args.size(); i++) + arguments[i] = castType(parameterTypes[i], args.get(i)); + + return arguments; + } + + private Object castType(Class targetType, String value) throws IllegalArgumentException + { + if (targetType == String.class) + return value.equals("null") ? "" : value; + else if (targetType == int.class || targetType == Integer.class) + return getNumber(value, Integer::parseInt, -1); + else if (targetType == long.class || targetType == Long.class) + return getNumber(value, Long::parseLong, -1); + else if (targetType == boolean.class || targetType == Boolean.class) + { + return getNumber(value, (v) -> { + if (!v.equals("true") && !v.equals("false")) + throw new IllegalStateException("Use 'true' or 'false' values for booleans"); + + return Boolean.parseBoolean(v); + }, false); + } + else if (targetType == Set.class) + { + if (value == null || value.equals("null")) + return new HashSet<>(); + else + { + return new LinkedHashSet<>(Arrays.asList(value.split(","))); + } + } + else + { + throw new IllegalArgumentException(format("unsupported type: %s", targetType)); + } + } + + private T getNumber(String value, Function transformer, T defaultValue) + { + if (value == null || value.equals("null")) + return defaultValue; + + try + { + return transformer.apply(value); + } + catch (NumberFormatException ex) + { + throw new IllegalStateException(format("Unable to parse value %s", value), ex); + } + } + } + + private static final Pattern CAMEL_PATTERN = Pattern.compile("([a-z])([A-Z])"); + + /** + * Special map for methods which do not adhere to camel-case convention precisely. + * These will be translated manually. + */ + private static final Map toSnakeCaseTranslationMap = new HashMap() + {{ + put("FieldsPerUDTFailThreshold", "fields_per_udt_fail_threshold"); + put("FieldsPerUDTWarnThreshold", "fields_per_udt_warn_threshold"); + put("FieldsPerUDTThreshold", "fields_per_udt_threshold"); + }}; + + private static final Map toCamelCaseTranslationMap = new HashMap() + {{ + put("set_fields_per_udt_threshold", "setFieldsPerUDTThreshold"); + }}; + + @VisibleForTesting + public enum GuardrailCategory + { + values, + thresholds, + flags, + others; + + public static GuardrailCategory parseCategory(String category, PrintStream out) + { + if (category == null) + return null; + + try + { + return GuardrailCategory.valueOf(category.toLowerCase()); + } + catch (IllegalArgumentException ex) + { + String enabledValues = Arrays.stream(GuardrailCategory.values()) + .map(GuardrailCategory::name) + .collect(Collectors.joining(",")); + out.printf("%nError: Illegal value for -c/--category used: '" + + category + "'. Supported values are " + enabledValues + ".%n"); + System.exit(1); + return null; + } + } + } + + void display(NodeProbe probe, List methods, GuardrailCategory userCategory) + { + try + { + List flags = new ArrayList<>(); + List thresholds = new ArrayList<>(); + List values = new ArrayList<>(); + List others = new ArrayList<>(); + + for (Method method : methods) + { + String guardrailName = toSnakeCase(method.getName().substring(3)); + + List bucket; + + if (guardrailName.endsWith("_enabled")) + bucket = flags; + else if (guardrailName.endsWith("_threshold")) + bucket = thresholds; + else if (guardrailName.endsWith("_disallowed") || + guardrailName.endsWith("_ignored") || + guardrailName.endsWith("_warned")) + bucket = values; + else + bucket = others; + + addRow(bucket, probe.getGuardrailsMBean(), method, guardrailName); + } + + TableBuilder tb = new TableBuilder(); + Map> holder = new LinkedHashMap<>(); + + holder.put(GuardrailCategory.flags, flags); + holder.put(GuardrailCategory.thresholds, thresholds); + holder.put(GuardrailCategory.values, values); + holder.put(GuardrailCategory.others, others); + + if (userCategory != null) + { + populateTable(tb, holder.get(userCategory)); + } + else + { + if (holder.values().stream().flatMap(list -> Stream.of(list.toArray(new InternalRow[0]))).count() == 1) + { + for (Map.Entry> entry : holder.entrySet()) + populateOne(tb, entry.getValue()); + } + else + { + for (Map.Entry> entry : holder.entrySet()) + populateTable(tb, entry.getValue()); + } + } + + tb.printTo(probe.output().out); + } + catch (Throwable e) + { + throw new RuntimeException("Error occured when getting the guardrails config", e); + } + } + + private void populateTable(TableBuilder tableBuilder, List bucket) + { + for (InternalRow row : bucket) + tableBuilder.add(row.name, row.value); + } + + private void populateOne(TableBuilder tableBuilder, List bucket) + { + if (bucket.size() == 1) + tableBuilder.add(bucket.get(0).value); + } + + void constructRow(List bucket, String guardrailName, String value) + { + bucket.add(new InternalRow(guardrailName, value)); + } + + abstract void addRow(List bucket, GuardrailsMBean mBean, Method method, String guardrailName) throws Throwable; + + public static class InternalRow + { + final String name; + final String value; + + public InternalRow(String name, String value) + { + this.name = name; + this.value = value; + } + } + + private static String toSnakeCase(String camelCase) + { + if (camelCase == null || camelCase.isEmpty()) + return camelCase; + else + { + String maybeSnakeCase = toSnakeCaseTranslationMap.get(camelCase); + if (maybeSnakeCase != null) + return maybeSnakeCase; + + return CAMEL_PATTERN.matcher(camelCase).replaceAll("$1_$2").toLowerCase(); + } + } + + private static String toCamelCase(String snakeCase) + { + if (snakeCase == null || snakeCase.isEmpty()) + return snakeCase; + + String maybeCamelCase = toCamelCaseTranslationMap.get(snakeCase); + if (maybeCamelCase != null) + return maybeCamelCase; + + StringBuilder result = new StringBuilder(); + boolean toUpper = false; + + for (int i = 0; i < snakeCase.length(); i++) + { + char c = snakeCase.charAt(i); + if (c == '_') + { + toUpper = true; + } + else + { + result.append(toUpper ? Character.toUpperCase(c) : c); + toUpper = false; + } + } + + return result.toString(); + } +} diff --git a/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java new file mode 100644 index 0000000000..a4320da548 --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java @@ -0,0 +1,342 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.tools.ToolRunner.ToolResult; +import org.apache.cassandra.tools.nodetool.GuardrailsConfigCommand.GuardrailCategory; + +import static org.apache.cassandra.tools.ToolRunner.invokeNodetool; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class GuardrailsConfigCommandsTest extends CQLTester +{ + @BeforeClass + public static void setup() throws Exception + { + requireNetwork(); + startJMXServer(); + } + + @Test + public void testGuardrailsConfigCommands() + { + ToolResult getResult = invokeNodetool("getguardrailsconfig"); + getResult.asserts().success(); + assertEquals(removeMultipleSpaces(ALL_GUARDRAILS_GETTER_OUTPUT), getOutput(getResult)); + + ToolResult getFlagsResult = invokeNodetool("getguardrailsconfig", "-c", "flags"); + getFlagsResult.asserts().success(); + assertEquals(removeMultipleSpaces(ALL_FLAGS_GETTER_OUTPUT), getOutput(getFlagsResult)); + + ToolResult getValuesResult = invokeNodetool("getguardrailsconfig", "-c", "values"); + getValuesResult.asserts().success(); + assertEquals(removeMultipleSpaces(ALL_VALUES_GETTER_OUTPUT), getOutput(getValuesResult)); + + ToolResult getThresholdsResult = invokeNodetool("getguardrailsconfig", "-c", "thresholds"); + getValuesResult.asserts().success(); + assertEquals(removeMultipleSpaces(ALL_THRESHOLDS_GETTER_OUTPUT), getOutput(getThresholdsResult)); + + ToolResult wrongCategory = invokeNodetool("getguardrailsconfig", "-c", "nonsense"); + wrongCategory.asserts().failure(); + assertTrue(getOutput(wrongCategory).contains("Error: Illegal value for -c/--category used: 'nonsense'. Supported values are values,thresholds,flags,others.")); + + // individual guardrail + ToolResult individualResult = invokeNodetool("getguardrailsconfig", "group_by_enabled"); + getResult.asserts().success(); + assertEquals("true\n", getOutput(individualResult)); + + // more than one result + ToolResult multipleResult = invokeNodetool("getguardrailsconfig", "group_by_enabled", "keyspaces_fail_threshold"); + getResult.asserts().success(); + assertEquals("group_by_enabled true\nkeyspaces_fail_threshold -1\n", getOutput(multipleResult)); + + // category with individual + ToolResult categoryWithIndividualResult = invokeNodetool("getguardrailsconfig", "-c", "values", "group_by_enabled"); + categoryWithIndividualResult.asserts().failure(); + assertTrue(categoryWithIndividualResult.getStdout().contains("Do not specify additional arguments when --category/-c is set.")); + + // get config of all guardrails enumerated on command line + String[] allLines = ALL_GUARDRAILS_GETTER_OUTPUT.split("\n"); + + List argsForAllGuardrails = new ArrayList<>(); + argsForAllGuardrails.add("getguardrailsconfig"); + + for (String line : allLines) + argsForAllGuardrails.add(line.split(" ")[0]); + + ToolResult allGuardrails = invokeNodetool(argsForAllGuardrails); + allGuardrails.asserts().success(); + assertEquals(ALL_GUARDRAILS_GETTER_OUTPUT, removeMultipleSpaces(allGuardrails.getStdout())); + + // set + + ToolResult setResultNoArgs = invokeNodetool("setguardrailsconfig"); + setResultNoArgs.asserts().failure(); + assertTrue(getOutput(setResultNoArgs).contains("No arguments.")); + + ToolResult setResultList = invokeNodetool("setguardrailsconfig", "--list"); + setResultList.asserts().success(); + setResultList = invokeNodetool("setguardrailsconfig", "-l"); + setResultList.asserts().success(); + assertEquals(removeMultipleSpaces(ALL_GUARDRAILS_SETTER_OUTPUT), getOutput(setResultList)); + + ToolResult categoryList = invokeNodetool("setguardrailsconfig", "--list", "--category", "values"); + categoryList.asserts().success(); + + for (GuardrailCategory category : GuardrailCategory.values()) + { + categoryList = invokeNodetool("setguardrailsconfig", "-l", "-c", category.name()); + categoryList.asserts().success(); + + String expectedOutput = null; + switch (category) + { + case flags: + expectedOutput = ALL_FLAGS_SETTER_OUTPUT; + break; + case values: + expectedOutput = ALL_VALUES_SETTER_OUTPUT; + break; + case thresholds: + expectedOutput = ALL_THRESHOLDS_SETTER_OUTPUT; + break; + case others: + expectedOutput = ALL_OTHER_SETTER_OUTPUT; + break; + default: + fail("Untested category " + category); + } + assertEquals(removeMultipleSpaces(expectedOutput), getOutput(categoryList)); + } + + // test -c without -l does not make sense + ToolResult emptyCategoryListing = invokeNodetool("setguardrailsconfig", "-l", "-c"); + emptyCategoryListing.asserts().failure(); + assertTrue(getOutput(emptyCategoryListing).contains("Required values for option 'guardrailCategory' not provided")); + + // test -c alone does not make sense + ToolResult onlyCategory = invokeNodetool("setguardrailsconfig", "-c", "values"); + onlyCategory.asserts().failure(); + assertTrue(getOutput(onlyCategory).contains("--category/-c can be specified only together with --list/-l")); + + // it would be quite cumbersome to test all guardrails are settable so we will + // set one from each category to prove the point + + // flag + setFlag("allow_filtering_enabled", false); + assertFalse(getFlag("allow_filtering_enabled")); + setFlag("allow_filtering_enabled", true); + assertTrue(getFlag("allow_filtering_enabled")); + + // value + setValues("table_properties_warned", "comment", "cdc"); + assertArrayEquals(new String[]{ "comment", "cdc" }, getValues("table_properties_warned")); + setValues("table_properties_warned", "null"); + assertArrayEquals(new String[0], getValues("table_properties_warned")); + + // threshold + setThresholds("keyspaces_threshold", "10", "20"); + assertEquals("20", getThreshold("keyspaces_fail_threshold")); + assertEquals("10", getThreshold("keyspaces_warn_threshold")); + setThresholds("keyspaces_threshold", "-1", "-1"); + assertEquals("-1", getThreshold("keyspaces_fail_threshold")); + assertEquals("-1", getThreshold("keyspaces_warn_threshold")); + + // test incorrect number of parameters or invalid values + ToolResult invalidNumberOfArgsForThreshold = invokeNodetool("setguardrailsconfig", "keyspaces_threshold", "10", "20", "30"); + invalidNumberOfArgsForThreshold.asserts().failure(); + assertTrue(invalidNumberOfArgsForThreshold.getStdout().contains("keyspaces_threshold is expecting 2 argument values. Getting 3 instead.")); + + // not separated by comma + ToolResult invalidNumberOfArgsForValues = invokeNodetool("setguardrailsconfig", "table_properties_warned", "comment", "cdc"); + invalidNumberOfArgsForValues.asserts().failure(); + assertTrue(invalidNumberOfArgsForValues.getStdout().contains("table_properties_warned is expecting 1 argument values. Getting 2 instead.")); + + // invalid boolean + ToolResult invalidBooleanForFlags = invokeNodetool("setguardrailsconfig", "allow_filtering_enabled", "nonsense"); + invalidBooleanForFlags.asserts().failure(); + assertTrue(invalidBooleanForFlags.getStdout().contains("Use 'true' or 'false' values for booleans")); + + // test propagation of errors from guardrail when values are wrong + ToolResult nonsenseSetterArgs = invokeNodetool("setguardrailsconfig", "keyspaces_threshold", "-10", "-20"); + nonsenseSetterArgs.asserts().failure(); + assertTrue(nonsenseSetterArgs.getStdout().contains("Error occured when setting the config for setter keyspaces_threshold with arguments [-10, -20]: " + + "Invalid value -10 for keyspaces_warn_threshold: negative values are not allowed, outside of -1 which disables the guardrail")); + } + + private static final String ALL_FLAGS_GETTER_OUTPUT = + "allow_filtering_enabled true\n" + + "compact_tables_enabled true\n" + + "drop_truncate_table_enabled true\n" + + "group_by_enabled true\n" + + "read_before_write_list_operations_enabled true\n" + + "secondary_indexes_enabled true\n" + + "uncompressed_tables_enabled true\n" + + "user_timestamps_enabled true\n"; + + private static final String ALL_THRESHOLDS_GETTER_OUTPUT = + "collection_size_fail_threshold null\n" + + "collection_size_warn_threshold null\n" + + "columns_per_table_fail_threshold -1 \n" + + "columns_per_table_warn_threshold -1 \n" + + "data_disk_usage_percentage_fail_threshold -1 \n" + + "data_disk_usage_percentage_warn_threshold -1 \n" + + "fields_per_udt_fail_threshold -1 \n" + + "fields_per_udt_warn_threshold -1 \n" + + "in_select_cartesian_product_fail_threshold -1 \n" + + "in_select_cartesian_product_warn_threshold -1 \n" + + "items_per_collection_fail_threshold -1 \n" + + "items_per_collection_warn_threshold -1 \n" + + "keyspaces_fail_threshold -1 \n" + + "keyspaces_warn_threshold -1 \n" + + "materialized_views_per_table_fail_threshold -1 \n" + + "materialized_views_per_table_warn_threshold -1 \n" + + "minimum_replication_factor_fail_threshold -1 \n" + + "minimum_replication_factor_warn_threshold -1 \n" + + "page_size_fail_threshold -1 \n" + + "page_size_warn_threshold -1 \n" + + "partition_keys_in_select_fail_threshold -1 \n" + + "partition_keys_in_select_warn_threshold -1 \n" + + "secondary_indexes_per_table_fail_threshold -1 \n" + + "secondary_indexes_per_table_warn_threshold -1 \n" + + "tables_fail_threshold -1 \n" + + "tables_warn_threshold -1 \n"; + + private static final String ALL_VALUES_GETTER_OUTPUT = + "read_consistency_levels_disallowed [] \n" + + "read_consistency_levels_warned [] \n" + + "table_properties_disallowed [] \n" + + "table_properties_ignored [] \n" + + "table_properties_warned [] \n" + + "write_consistency_levels_disallowed [] \n" + + "write_consistency_levels_warned [] \n"; + + private static final String ALL_OTHER_GETTER_OUTPUT = + "data_disk_usage_max_disk_size null \n"; + + private static final String ALL_FLAGS_SETTER_OUTPUT = + "allow_filtering_enabled boolean\n" + + "compact_tables_enabled boolean\n" + + "drop_truncate_table_enabled boolean\n" + + "group_by_enabled boolean\n" + + "read_before_write_list_operations_enabled boolean\n" + + "secondary_indexes_enabled boolean\n" + + "uncompressed_tables_enabled boolean\n" + + "user_timestamps_enabled boolean\n"; + + private static final String ALL_THRESHOLDS_SETTER_OUTPUT = + "collection_size_threshold [java.lang.String, java.lang.String]\n" + + "columns_per_table_threshold [int, int] \n" + + "data_disk_usage_percentage_threshold [int, int] \n" + + "fields_per_udt_threshold [int, int] \n" + + "in_select_cartesian_product_threshold [int, int] \n" + + "items_per_collection_threshold [int, int] \n" + + "keyspaces_threshold [int, int] \n" + + "materialized_views_per_table_threshold [int, int] \n" + + "minimum_replication_factor_threshold [int, int] \n" + + "page_size_threshold [int, int] \n" + + "partition_keys_in_select_threshold [int, int] \n" + + "secondary_indexes_per_table_threshold [int, int] \n" + + "tables_threshold [int, int] \n"; + + private static final String ALL_VALUES_SETTER_OUTPUT = + "read_consistency_levels_disallowed java.util.Set\n" + + "read_consistency_levels_warned java.util.Set\n" + + "table_properties_disallowed java.util.Set\n" + + "table_properties_ignored java.util.Set\n" + + "table_properties_warned java.util.Set\n" + + "write_consistency_levels_disallowed java.util.Set\n" + + "write_consistency_levels_warned java.util.Set\n"; + + private static final String ALL_OTHER_SETTER_OUTPUT = + "data_disk_usage_max_disk_size java.lang.String \n"; + + + private static final String ALL_GUARDRAILS_GETTER_OUTPUT = removeMultipleSpaces(ALL_FLAGS_GETTER_OUTPUT + + ALL_THRESHOLDS_GETTER_OUTPUT + + ALL_VALUES_GETTER_OUTPUT + + ALL_OTHER_GETTER_OUTPUT); + + private static final String ALL_GUARDRAILS_SETTER_OUTPUT = removeMultipleSpaces(ALL_FLAGS_SETTER_OUTPUT + + ALL_THRESHOLDS_SETTER_OUTPUT + + ALL_VALUES_SETTER_OUTPUT + + ALL_OTHER_SETTER_OUTPUT); + + private static String removeMultipleSpaces(String input) + { + return input.replaceAll(" +", " ").replaceAll(" \n", "\n"); + } + + private String getOutput(ToolResult toolResult) + { + return removeMultipleSpaces(toolResult.getStdout()); + } + + private ToolResult setFlag(String name, Boolean flag) + { + return invokeNodetool("setguardrailsconfig", name, flag.toString()); + } + + private ToolResult setThresholds(String name, String warn, String fail) + { + return invokeNodetool("setguardrailsconfig", name, warn, fail); + } + + private ToolResult setValues(String name, String... values) + { + return invokeNodetool("setguardrailsconfig", name, String.join(",", Arrays.asList(values))); + } + + private boolean getFlag(String name) + { + return Boolean.parseBoolean(invokeNodetool("getguardrailsconfig", name).getStdout().replaceAll("\n", "")); + } + + private String getThreshold(String name) + { + return invokeNodetool("getguardrailsconfig", name).getStdout().replaceAll("\n", ""); + } + + private String[] getValues(String name) + { + String[] split = invokeNodetool("getguardrailsconfig", name).getStdout() + .replace("\n", "") + .replace("[", "") + .replace("]", "") + .replace(" ", "") + .split(","); + + if (split.length == 1 && split[0].isEmpty()) + return new String[0]; + else + return split; + } +}