From 1382d33d12a4c396d87b7bd7d9eb993b3060a472 Mon Sep 17 00:00:00 2001 From: Jonathan Shook Date: Thu, 29 Oct 2020 10:55:38 -0500 Subject: [PATCH] argsfiles and statedirs --- .../io/nosqlbench/engine/cli/ArgsFile.java | 234 -------- .../java/io/nosqlbench/engine/cli/NBCLI.java | 64 +-- .../nosqlbench/engine/cli/NBCLIArgsFile.java | 500 ++++++++++++++++++ .../nosqlbench/engine/cli/NBCLIOptions.java | 152 ++++-- .../nosqlbench/engine/cli/ArgsFileTest.java | 40 -- .../engine/cli/NBCLIArgsFileTest.java | 117 ++++ 6 files changed, 768 insertions(+), 339 deletions(-) delete mode 100644 engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java create mode 100644 engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIArgsFile.java delete mode 100644 engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java create mode 100644 engine-cli/src/test/java/io/nosqlbench/engine/cli/NBCLIArgsFileTest.java diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java deleted file mode 100644 index 94dcd4258..000000000 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/ArgsFile.java +++ /dev/null @@ -1,234 +0,0 @@ -package io.nosqlbench.engine.cli; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - *

Synopsis

- * - * ArgsFile is a command-line modifier which can take linked list of - * command args and modify it, and/or modify argsfile refrenced in this way. - * - *

ArgsFile Selection

- * - * During processing, any occurence of '--argsfile' selects the active argsfile and loads - * it into the command line in place of the '--argsfile' argument. By default the args file - * will be loaded if it exists, and a warning will be given if it does not. - * - * The '--argsfile-required <somepath>' version will throw an error if the args file - * is not present, but it will not report any warnings or details otherwise. - * - * The `--argsfile-optional <somepath> version will not throw an error if the args - * file is not present, and it will not report any warnings or details otherwise. - * - * A prefix command line can be given to ArgsFile to pre-load any settings. In this way - * it is possible to easily provide a default args file which will be loaded. For example, - * A prefix command of '--argsfile-optional <somepath>' will load options if they are - * available in the specified file, but will otherwise provide no feedback to the user. - * - *

ArgsFile Injection

- * - * When an argsfile is loaded, it reads a command from each line into the current position - * of the command line. No parsing is done. Blank lines are ignored. Newlines are used as the - * argument delimiter, and lines that end with a backslash before the newline are automatically - * joined together. - * - *

ArgsFile Diagnostics

- * - * All modifications to the command line should be reported to the logging facility at - * INFO level. This assumes that the calling layer wants to inform users of command line injections, - * and that the user can select to be notified of warnings only if desired. - * - *

Environment Variables

- * - * Simple environment variable substitution is attempted for any pattern which appears as '$' followed - * by all uppercase letters and underscores. Any references of this type which are not resolvable - * will cause an error to be thrown. - */ -public class ArgsFile { - private final static Logger logger = LoggerFactory.getLogger(ArgsFile.class); - - private Path argsPath; - private LinkedList preload; - - public ArgsFile() { - } - - public ArgsFile preload(String... preload) { - this.preload = new LinkedList(Arrays.asList(preload)); - return this; - } - - private enum Selection { - // Ignore if not present, show injections at info - IgnoreIfMissing, - // Warn if not present, but show injections at info - WarnIfMissing, - // throw error if not present, show injections at info - ErrorIfMissing - } - - public LinkedList process(String... args) { - return process(new LinkedList(Arrays.asList(args))); - } - - public LinkedList process(LinkedList commandline) { - if (preload != null) { - LinkedList modified = new LinkedList(); - modified.addAll(preload); - modified.addAll(commandline); - preload = null; - commandline = modified; - } - LinkedList composed = new LinkedList<>(); - while (commandline.peekFirst() != null) { - String arg = commandline.peekFirst(); - switch (arg) { - case "--argsfile": - commandline.removeFirst(); - String argspath = readWordOrThrow(commandline, "path to an args file"); - setArgsFile(argspath, Selection.WarnIfMissing); - commandline = loadArgs(this.argsPath, Selection.WarnIfMissing, commandline); - break; - case "--argsfile-required": - commandline.removeFirst(); - String argspathRequired = readWordOrThrow(commandline, "path to an args file"); - setArgsFile(argspathRequired, Selection.ErrorIfMissing); - commandline = loadArgs(this.argsPath, Selection.ErrorIfMissing, commandline); - break; - case "--argsfile-optional": - commandline.removeFirst(); - String argspathOptional = readWordOrThrow(commandline, "path to an args file"); - setArgsFile(argspathOptional, Selection.IgnoreIfMissing); - commandline = loadArgs(this.argsPath, Selection.IgnoreIfMissing, commandline); - break; - case "--pin": - commandline.removeFirst(); - commandline = pinArg(commandline); - break; - case "--unpin": - commandline.removeFirst(); - commandline = unpinArg(commandline); - break; - default: - composed.addLast(commandline.removeFirst()); - } - - } - return composed; - } - - private LinkedList loadArgs(Path argspath, Selection mode, LinkedList commandline) { - if (!assertArgsFileExists(argspath, mode)) { - return commandline; - } - List lines = null; - try { - lines = Files.readAllLines(argspath); - } catch (IOException e) { - throw new RuntimeException(e); - } - List content = lines.stream() - .filter(s -> !s.startsWith("#")) - .filter(s -> !s.startsWith("/")) - .filter(s -> !s.isBlank()) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); - StringBuilder splitword = new StringBuilder(); - LinkedList loaded = new LinkedList<>(); - for (String s : content) { - splitword.append(s); - if (!s.endsWith("\\")) { - loaded.addLast(splitword.toString()); - splitword.setLength(0); - } else { - splitword.setLength(splitword.length() - 1); - } - } - if (splitword.length() > 0) { - throw new RuntimeException("unqualified line continuation for '" + splitword.toString() + "'"); - } - - Iterator injections = loaded.descendingIterator(); - while (injections.hasNext()) { - String injection = injections.next(); - injection = injectEnv(injection); - commandline.addFirst(injection); - } - - return commandline; - } - - private boolean assertArgsFileExists(Path argspath, Selection mode) { - if (!Files.exists(argsPath)) { - switch (mode) { - case ErrorIfMissing: - throw new RuntimeException("A required argsfile was specified, but it does not exist: '" + argspath + "'"); - case WarnIfMissing: - logger.warn("An argsfile was specified, but it does not exist: '" + argspath + "'"); - case IgnoreIfMissing: - } - return false; - } - return true; - } - - private void setArgsFile(String argspath, Selection mode) { - this.argsPath = Path.of(argspath); -// assertIfMissing(this.argsPath,mode); - } - - private String readWordOrThrow(LinkedList commandline, String description) { - String found = commandline.peekFirst(); - if (found == null) { - throw new RuntimeException("Unable to read argument top option for " + description); - } - return commandline.removeFirst(); - } - - private LinkedList pinArg(LinkedList commandline) { - if (this.argsPath == null) { - throw new RuntimeException("No argsfile has been selected before using the pin option."); - } - return commandline; - } - - private LinkedList unpinArg(LinkedList commandline) { - if (this.argsPath == null) { - throw new RuntimeException("No argsfile has been selected before using the unpin option."); - } - return commandline; - } - - private String injectEnv(String word) { - Pattern envpattern = Pattern.compile("(?\\$[A-Z_]+)"); - Matcher matcher = envpattern.matcher(word); - StringBuilder sb = new StringBuilder(); - while (matcher.find()) { - String envvar = matcher.group("envvar"); - String value = System.getenv(envvar); - if (value == null) { - throw new RuntimeException("Env var '" + envvar + "' was not found in the environment."); - } - matcher.appendReplacement(sb, value); - } - matcher.appendTail(sb); - return sb.toString(); - } - - public LinkedList pin(LinkedList arglist) { - return arglist; - } - - public LinkedList unpin(LinkedList arglist) { - return arglist; - } -} diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java index 57caa129d..db6f08530 100644 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLI.java @@ -39,10 +39,10 @@ import java.util.stream.Collectors; public class NBCLI { - private static final Logger logger = LoggerFactory.getLogger(NBCLI.class); + private static final Logger logger = LoggerFactory.getLogger("NBCLI"); private static final Logger EVENTS = LoggerFactory.getLogger("EVENTS"); - private static final String CHART_HDR_LOG_NAME = "hdrdata-for-chart.log"; + private static final String CHART_HDR_LOG_NAME = "hdrdata-for-chart.log"; private final String commandName; @@ -70,36 +70,43 @@ public class NBCLI { NBCLIOptions globalOptions = new NBCLIOptions(args, NBCLIOptions.Mode.ParseGlobalsOnly); // Global only processing + if (args.length == 0) { + System.out.println(loadHelpFile("commandline.md")); + System.exit(0); + } + boolean dockerMetrics = globalOptions.wantsDockerMetrics(); + String dockerMetricsAt = globalOptions.wantsDockerMetricsAt(); String reportGraphiteTo = globalOptions.wantsReportGraphiteTo(); + int mOpts = (dockerMetrics ? 1 : 0) + (dockerMetricsAt != null ? 1 : 0) + (reportGraphiteTo != null ? 1 : 0); + if (mOpts > 1) { + throw new BasicError("You have multiple conflicting options which attempt to set\n" + + " the destination for metrics and annotations. Please select only one of\n" + + " --docker-metrics, --docker-metrics-at , or --report-graphite-to \n" + + " For more details, see run 'nb help docker-metrics'"); + } - if (globalOptions.wantsDockerMetrics()) { + String metricsAddr = null; + + if (dockerMetrics) { + // Setup docker stack for local docker metrics logger.info("Docker metrics is enabled. Docker must be installed for this to work"); DockerMetricsManager dmh = new DockerMetricsManager(); Map dashboardOptions = Map.of( - DockerMetricsManager.GRAFANA_TAG, globalOptions.getDockerGrafanaTag() + DockerMetricsManager.GRAFANA_TAG, globalOptions.getDockerGrafanaTag() ); dmh.startMetrics(dashboardOptions); - String warn = "Docker Containers are started, for grafana and prometheus, hit" + - " these urls in your browser: http://:3000 and http://:9090"; + " these urls in your browser: http://:3000 and http://:9090"; logger.warn(warn); - if (reportGraphiteTo != null) { - logger.warn(String.format("Docker metrics are enabled (--docker-metrics)" + - " but graphite reporting (--report-graphite-to) is set to %s \n" + - "usually only one of the two is configured.", - reportGraphiteTo)); - } else { - logger.info("Setting graphite reporting to localhost"); - reportGraphiteTo = "localhost:9109"; - } + metricsAddr = "localhost"; + } else if (dockerMetricsAt != null) { + metricsAddr = dockerMetricsAt; + } - if (globalOptions.getAnnotatorsConfig() != null) { - logger.warn("Docker metrics and separate annotations" + - "are configured (both --docker-metrics and --annotations)."); - } else { - Annotators.init("grafana{http://localhost:3000/}"); - } + if (metricsAddr != null) { + reportGraphiteTo = metricsAddr + ":9109"; + Annotators.init("{type:'grafana',url:'http://" + metricsAddr + ":3000/'}"); } if (args.length > 0 && args[0].toLowerCase().equals("virtdata")) { @@ -275,13 +282,6 @@ public class NBCLI { // intentionally not shown for warn-only logger.info("console logging level is " + options.wantsConsoleLogLevel()); - if (options.getCommands(). - - size() == 0) { - System.out.println(loadHelpFile("commandline.md")); - System.exit(0); - } - ScenariosExecutor executor = new ScenariosExecutor("executor-" + sessionName, 1); Scenario scenario = new Scenario( @@ -311,8 +311,6 @@ public class NBCLI { } - // Execute Scenario! - Level consoleLogLevel = options.wantsConsoleLogLevel(); Level scenarioLogLevel = Level.toLevel(options.getLogsLevel()); if (scenarioLogLevel.toInt() > consoleLogLevel.toInt()) { @@ -321,6 +319,12 @@ public class NBCLI { Level maxLevel = Level.toLevel(Math.min(consoleLogLevel.toInt(), scenarioLogLevel.toInt())); + // Execute Scenario! + if (options.getCommands().size() == 0) { + logger.info("No commands provided. Exiting before scenario."); + System.exit(0); + } + scenario.addScriptText(scriptData); ScriptParams scriptParams = new ScriptParams(); scriptParams.putAll(buffer.getCombinedParams()); diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIArgsFile.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIArgsFile.java new file mode 100644 index 000000000..ec047e94f --- /dev/null +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIArgsFile.java @@ -0,0 +1,500 @@ +package io.nosqlbench.engine.cli; + +import io.nosqlbench.nb.api.errors.BasicError; +import joptsimple.internal.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

Synopsis

+ * + * ArgsFile is a command-line modifier which can take linked list of + * command args and modify it, and/or modify argsfile refrenced in this way. + * + *

ArgsFile Selection

+ * + * During processing, any occurence of '--argsfile' selects the active argsfile and loads + * it into the command line in place of the '--argsfile' argument. By default the args file + * will be loaded if it exists, and a warning will be given if it does not. + * + * The '--argsfile-required <somepath>' version will throw an error if the args file + * is not present, but it will not report any warnings or details otherwise. + * + * The `--argsfile-optional <somepath> version will not throw an error if the args + * file is not present, and it will not report any warnings or details otherwise. + * + * A prefix command line can be given to ArgsFile to pre-load any settings. In this way + * it is possible to easily provide a default args file which will be loaded. For example, + * A prefix command of '--argsfile-optional <somepath>' will load options if they are + * available in the specified file, but will otherwise provide no feedback to the user. + * + *

ArgsFile Injection

+ * + * When an argsfile is loaded, it reads a command from each line into the current position + * of the command line. No parsing is done. Blank lines are ignored. Newlines are used as the + * argument delimiter, and lines that end with a backslash before the newline are automatically + * joined together. + * + *

ArgsFile Diagnostics

+ * + * All modifications to the command line should be reported to the logging facility at + * INFO level. This assumes that the calling layer wants to inform users of command line injections, + * and that the user can select to be notified of warnings only if desired. + * + *

Environment Variables

+ * + * Simple environment variable substitution is attempted for any pattern which appears as '$' followed + * by all uppercase letters and underscores. Any references of this type which are not resolvable + * will cause an error to be thrown. + */ +public class NBCLIArgsFile { + private final static Logger logger = LoggerFactory.getLogger("ARGSFILE"); + + // Options which may contextualize other CLI options or commands. + // These must be parsed first + public static final String ARGS_FILE = "--argsfile"; + public static final String ARGS_FILE_OPTIONAL = "--argsfile-optional"; + public static final String ARGS_FILE_REQUIRED = "--argsfile-required"; + public static final String ARGS_PIN = "--pin"; + public static final String ARGS_UNPIN = "--unpin"; + + private Path argsPath; + private LinkedList preload; + private final Set stopWords = new HashSet<>(); + private final LinkedHashSet args = new LinkedHashSet<>(); + LinkedHashSet argsToPin = new LinkedHashSet<>(); + LinkedHashSet argsToUnpin = new LinkedHashSet<>(); + private final Set readPaths = new HashSet<>(); + + public NBCLIArgsFile() { + } + + public NBCLIArgsFile preload(String... preload) { + this.preload = new LinkedList(Arrays.asList(preload)); + return this; + } + + /** + * Indicate which words are invalid for the purposes of matching + * trailing parts of arguments. The provided words will not + * be considered as valid values to arguments in any case. + * + * @param reservedWords Words to ignore in option values + * @return this ArgsFile, for method chaining + */ + public NBCLIArgsFile reserved(Collection reservedWords) { + this.stopWords.addAll(reservedWords); + return this; + } + + /** + * Indicate which words are invalid for the purposes of matching + * trailing parts of arguments. The provided words will not + * be considered as valid values to arguments in any case. + * + * @param reservedWords Words to ignore in option values + * @return this ArgsFile, for method chaining + */ + public NBCLIArgsFile reserved(String... reservedWords) { + this.stopWords.addAll(Arrays.asList(reservedWords)); + return this; + } + + private enum Selection { + // Ignore if not present, show injections at info + IgnoreIfMissing, + // Warn if not present, but show injections at info + WarnIfMissing, + // throw error if not present, show injections at info + ErrorIfMissing + } + + public LinkedList process(String... args) { + return process(new LinkedList(Arrays.asList(args))); + } + + public LinkedList process(LinkedList commandline) { + if (preload != null) { + LinkedList modified = new LinkedList(); + modified.addAll(preload); + modified.addAll(commandline); + preload = null; + commandline = modified; + } + LinkedList composed = new LinkedList<>(); + + while (commandline.peekFirst() != null) { + String arg = commandline.peekFirst(); + switch (arg) { + case ARGS_FILE: + pinAndUnpin(); + commandline.removeFirst(); + String argspath = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspath, Selection.WarnIfMissing); + commandline = mergeArgs(this.argsPath, Selection.WarnIfMissing, commandline); + break; + case ARGS_FILE_REQUIRED: + commandline.removeFirst(); + String argspathRequired = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspathRequired, Selection.ErrorIfMissing); + commandline = mergeArgs(this.argsPath, Selection.ErrorIfMissing, commandline); + break; + case ARGS_FILE_OPTIONAL: + commandline.removeFirst(); + String argspathOptional = readWordOrThrow(commandline, "path to an args file"); + setArgsFile(argspathOptional, Selection.IgnoreIfMissing); + commandline = mergeArgs(this.argsPath, Selection.IgnoreIfMissing, commandline); + break; + case ARGS_PIN: + commandline.removeFirst(); + argsToPin.addAll(argsToLines(readOptionAndArg(commandline, false))); + break; + case ARGS_UNPIN: + commandline.removeFirst(); + argsToUnpin.addAll(argsToLines(readOptionAndArg(commandline, true))); + break; + default: + composed.addLast(commandline.removeFirst()); + } + } + pinAndUnpin(); + return composed; + } + + private void pinAndUnpin() { + if (this.argsToUnpin.size() == 0 && this.argsToPin.size() == 0) { + return; + } + LinkedHashSet extant = readArgsFile(this.argsPath, Selection.IgnoreIfMissing); + LinkedHashSet mergedPins = mergePins(this.argsToPin, this.argsToUnpin, extant); + if (extant.equals(mergedPins)) { + logger.info("Pinning resulted in no changes to argsfile '" + this.argsPath.toString() + "'"); + } else { + logger.info("Writing updated argsfile '" + this.argsPath.toString() + "' with " + + (this.argsToPin.size() + this.argsToUnpin.size()) + " changes"); + writeArgsFile(mergedPins); + } + + this.argsToPin.clear(); + this.argsToUnpin.clear(); + + } + + + private LinkedHashSet mergePins( + LinkedHashSet toPin, + LinkedHashSet toUnpin, + LinkedHashSet extant) { + + LinkedHashSet merged = new LinkedHashSet<>(); + merged.addAll(extant); + + for (String arg : toPin) { + if (argsToUnpin.contains(arg)) { + throw new RuntimeException("You have both --pin and --unpin for '" + arg + ", I don't know which " + + "one you want."); + } + } + for (String arg : toUnpin) { + if (argsToPin.contains(arg)) { + throw new RuntimeException("You have both --pin and --unpin for '" + arg + ", I don't know which " + + "one you want."); + } + } + + for (String toAdd : toPin) { + if (merged.contains(toAdd)) { + logger.warn("Requested to pin argument again: '" + toAdd + "', ignoring"); + } else { + logger.info("Pinning option '" + toAdd + "' to '" + this.argsPath.toString() + "'"); + merged.add(toAdd); + } + } + + for (String toDel : toUnpin) { + if (merged.contains(toDel)) { + logger.info("Unpinning '" + toDel + "' from '" + this.argsPath.toString() + "'"); + merged.remove(toDel); + } else { + logger.warn("Requested to unpin argument '" + toDel + "' which was not found in " + argsPath.toString()); + } + } + + return merged; + } + + LinkedList mergeArgs(Path argspath, Selection mode, LinkedList commandline) { + this.args.clear(); + if (this.readPaths.contains(argsPath.toString())) { + throw new BasicError("Recursive reading of argsfile is detected for '" + argspath.toString() + "'.\n" + + "Please ensure that you do not have cyclic references in your arguments for argsfiles."); + } + LinkedHashSet loaded = readArgsFile(argspath, mode); + this.readPaths.add(argsPath.toString()); + + List interpolated = loaded.stream() + .map(p -> { + String q = Environment.INSTANCE.interpolate(p).orElse(p); + if (!q.equals(p)) { + logger.info("argsfile: '" + argsPath.toString() + "': loaded option '" + p + "' as '" + q + "'"); + } + return q; + }) + .collect(Collectors.toList()); + + LinkedList inArgvForm = linesToArgs(interpolated); + this.args.addAll(inArgvForm); + return concat(inArgvForm, commandline); + } + + private LinkedList concat(Collection... entries) { + LinkedList composed = new LinkedList<>(); + for (Collection list : entries) { + composed.addAll(list); + } + return composed; + } + + /** + *

Load the args file into an args array. The returned format follows + * the standard pattern of args as you would see for a main method, although + * the internal format is structured to support easy editing and clarity.

+ * + *

+ * The args file is stored in a simple option-per-line format which + * follows these rules: + *

    + *
  • Lines ending with a backslash (\) only are concatenated to the next + * line with the backslash removed. + *
  • Line content consists of one option and one optional argument.
  • + *
  • Options must start with at least one dash (-).
  • + *
  • If an argument is provided for an option, it follows the option and a space.
  • + *
  • Empty lines and lines which start with '//' or '#' are ignored.
  • + *
  • Lines which are identical after applying the above rules are elided + * down to the last occurence.
  • + *
+ *

+ * + *

+ * This allows for multi-valued options, or options which can be specified multiple + * times with different arguments to be supported, so long as each occurrence has a + * unique option value. + *

+ * + * @param argspath The path of the argsfile to load + * @param mode The level of feedback to provide in the case of a missing file + * @return The argsfile content, structured like an args array + */ + private LinkedHashSet readArgsFile(Path argspath, Selection mode) { + LinkedHashSet args = new LinkedHashSet<>(); + + if (!assertArgsFileExists(argspath, mode)) { + return args; + } + + List lines = null; + try { + lines = Files.readAllLines(argspath); + } catch (IOException e) { + throw new RuntimeException(e); + } + List content = lines.stream() + .filter(s -> !s.startsWith("#")) + .filter(s -> !s.startsWith("/")) + .filter(s -> !s.isBlank()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + StringBuilder splitword = new StringBuilder(); + LinkedHashSet loaded = new LinkedHashSet<>(); + for (String s : content) { + splitword.append(s); + if (!s.endsWith("\\")) { + loaded.add(splitword.toString()); + splitword.setLength(0); + } else { + splitword.setLength(splitword.length() - 1); + } + } + if (splitword.length() > 0) { + throw new RuntimeException("unqualified line continuation for '" + splitword.toString() + "'"); + } + + return loaded; + } + + /** + * Write the argsfile in the format specified by {@link #readArgsFile(Path, Selection)} + * + * This method requires that an argsFile has been set by a previous + * --argsfile or --argsfile-required or --argsfile-optional option. + * + * @param args The args to write in one-arg-per-line form + */ + private void writeArgsFile(LinkedHashSet args) { + if (this.argsPath == null) { + throw new RuntimeException("No argsfile has been selected before using the pin option."); + } + + try { + Files.createDirectories( + this.argsPath.getParent(), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwx---")) + ); + Files.write(this.argsPath, args); + } catch (IOException e) { + throw new BasicError("unable to write '" + this.argsPath + "': " + e.getMessage()); + } + } + + private boolean assertArgsFileExists(Path argspath, Selection mode) { + if (!Files.exists(argsPath)) { + switch (mode) { + case ErrorIfMissing: + throw new RuntimeException("A required argsfile was specified, but it does not exist: '" + argspath + "'"); + case WarnIfMissing: + logger.warn("An argsfile was specified, but it does not exist: '" + argspath + "'"); + case IgnoreIfMissing: + } + return false; + } + return true; + } + + private void setArgsFile(String argspath, Selection mode) { + Path selected = null; + String[] possibles = argspath.split(":"); + for (String possible : possibles) { + Optional expanded = Environment.INSTANCE.interpolate(possible); + if (expanded.isPresent()) { + Path possiblePath = Path.of(expanded.get()); + if (Files.exists(possiblePath)) { + selected = possiblePath; + break; + } + } + } + + if (selected == null) { + String defaultFirst = possibles[0]; + defaultFirst = Environment.INSTANCE.interpolate(defaultFirst) + .orElseThrow(() -> new RuntimeException("Invalid default argsfile: '" + possibles[0] + "'")); + selected = Path.of(defaultFirst); + } + + this.argsPath = selected; + logger.debug("argsfile path is now '" + this.argsPath.toString() + "'"); + } + + /** + * Convert argv arguments to consolidated form which is used in the args file. + * This means that options and their (optional) arguments are on the + * same line, concatenated with a space after the option. + * + * @return The arg-per-line form + */ + LinkedHashSet argsToLines(List args) { + LinkedHashSet lines = new LinkedHashSet<>(); + Iterator iter = args.iterator(); + List element = new ArrayList<>(); + while (iter.hasNext()) { + String word = iter.next(); + if (word.startsWith("-")) { + if (element.size() > 0) { + lines.add(Strings.join(element, " ")); + element.clear(); + } + } + element.add(word); + } + lines.add(Strings.join(element, " ")); + return lines; + } + + /** + * Convert arg lines as used in an args file to the argv which + * is used on the command line. + * + * @return The argv list as you would see with {@code main(String[] argv)} + */ + LinkedList linesToArgs(Collection lines) { + LinkedList args = new LinkedList<>(); + for (String line : lines) { + if (line.startsWith("-")) { + String[] words = line.split(" ", 2); + args.addAll(Arrays.asList(words)); + } else { + args.add(line); + } + } + return args; + } + + private LinkedList unpin(LinkedList arglist) { + if (this.argsPath == null) { + throw new RuntimeException("No argsfile has been selected before using the pin option."); + } + return arglist; + } + + /** + * Read the current command line option from the argument list, + * so long as it is a dash or double-dash option, and is not a + * reserved word, and any argument that goes with it, if any. + * + * @param arglist The command line containing the option + * @return A list containing the current command line option + */ + private LinkedList readOptionAndArg(LinkedList arglist, boolean consume) { + LinkedList option = new LinkedList<>(); + ListIterator iter = arglist.listIterator(); + + if (!iter.hasNext()) { + throw new RuntimeException("Arguments must follow the --pin option"); + } + String opt = iter.next(); + + if (!opt.startsWith("-") || stopWords.contains(opt)) { + throw new RuntimeException("Arguments following the --pin option must not" + + " be commands like '" + opt + "'"); + } + option.add(opt); + if (consume) { + iter.remove(); + } + + if (iter.hasNext()) { + opt = iter.next(); + if (!stopWords.contains(opt) && !opt.startsWith("-")) { + option.add(opt); + if (consume) { + iter.remove(); + } + } + } + return option; + } + + + /** + * Consume the next word from the beginning of the linked list. If there is + * no word to consume, then throw an error with the description. + * + * @param commandline A list of words + * @param description A description of what the next word value is meant to represent + * @return The next word from the list + */ + private String readWordOrThrow(LinkedList commandline, String description) { + String found = commandline.peekFirst(); + if (found == null) { + throw new RuntimeException("Unable to read argument top option for " + description); + } + return commandline.removeFirst(); + } +} diff --git a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java index c92aa52f3..fe0398152 100644 --- a/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java +++ b/engine-cli/src/main/java/io/nosqlbench/engine/cli/NBCLIOptions.java @@ -4,11 +4,15 @@ import ch.qos.logback.classic.Level; import io.nosqlbench.engine.api.metrics.IndicatorMode; import io.nosqlbench.engine.api.util.Unit; import io.nosqlbench.engine.core.script.Scenario; +import io.nosqlbench.nb.api.errors.BasicError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; import java.security.InvalidParameterException; import java.util.*; import java.util.stream.Collectors; @@ -19,19 +23,16 @@ import java.util.stream.Collectors; */ public class NBCLIOptions { - private final static String userHome = System.getProperty("user.home"); - private final static Path defaultOptFile = Path.of(userHome, ".nosqlbench/options"); - - private final static Logger logger = LoggerFactory.getLogger(NBCLIOptions.class); - - // Options which may contextualize other CLI options or commands. - // These must be parsed first - private static final String ARGS_FILE = "--argsfile"; - private static final String ARGS_FILE_DEFAULT = "$HOME/.nosqlbench/argsfile"; - private static final String ARGS_PIN = "--pin"; - private static final String ARGS_UNPIN = "--unpin"; + private final static Logger logger = LoggerFactory.getLogger("OPTIONS"); + private final static String NB_STATE_DIR = "--statedir"; + private final static String NB_STATEDIR_PATHS = "$NBSTATEDIR:$PWD/.nosqlbench:$HOME/.nosqlbench"; + public static final String ARGS_FILE_DEFAULT = "$NBSTATEDIR/argsfile"; private static final String INCLUDE = "--include"; + + private final static String userHome = System.getProperty("user.home"); + + private static final String METRICS_PREFIX = "--metrics-prefix"; // private static final String ANNOTATE_TO_GRAFANA = "--grafana-baseurl"; @@ -39,7 +40,6 @@ public class NBCLIOptions { private static final String ANNOTATORS_CONFIG = "--annotators"; private static final String DEFAULT_ANNOTATORS = "all"; - // Discovery private static final String HELP = "--help"; private static final String LIST_METRICS = "--list-metrics"; @@ -91,6 +91,7 @@ public class NBCLIOptions { private static final String DOCKER_GRAFANA_TAG = "--docker-grafana-tag"; private static final String DEFAULT_CONSOLE_LOGGING_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"; + public static final String NBSTATEDIR = "NBSTATEDIR"; private final LinkedList cmdList = new LinkedList<>(); private int logsMax = 0; @@ -130,13 +131,16 @@ public class NBCLIOptions { private Scenario.Engine engine = Scenario.Engine.Graalvm; private boolean graaljs_compat = false; private int hdr_digits = 4; - private String docker_grafana_tag = "7.0.1"; + private String docker_grafana_tag = "7.2.2"; private boolean showStackTraces = false; private boolean compileScript = false; private String scriptFile = null; private String[] annotateEvents = new String[]{"ALL"}; private String dockerMetricsHost; private String annotatorsConfig = ""; + private String statedirs = NB_STATEDIR_PATHS; + private Path statepath; + private List statePathAccesses = new ArrayList<>(); public String getAnnotatorsConfig() { return annotatorsConfig; @@ -163,8 +167,6 @@ public class NBCLIOptions { } private LinkedList parseGlobalOptions(String[] args) { - ArgsFile argsfile = new ArgsFile(); - argsfile.preload("--argsfile-optional", ARGS_FILE_DEFAULT); LinkedList arglist = new LinkedList<>() {{ addAll(Arrays.asList(args)); @@ -175,7 +177,8 @@ public class NBCLIOptions { return arglist; } - // Preprocess --include regardless of position + // Process --include and --statedir, separately first + // regardless of position LinkedList nonincludes = new LinkedList<>(); while (arglist.peekFirst() != null) { String word = arglist.peekFirst(); @@ -188,9 +191,53 @@ public class NBCLIOptions { } switch (word) { - case ARGS_FILE: - case ARGS_PIN: - case ARGS_UNPIN: + case NB_STATE_DIR: + arglist.removeFirst(); + this.statedirs = readWordOrThrow(arglist, "nosqlbench global state directory"); + break; + case INCLUDE: + arglist.removeFirst(); + String include = readWordOrThrow(arglist, "path to include"); + wantsToIncludePaths.add(include); + break; + default: + nonincludes.addLast(arglist.removeFirst()); + } + } + this.statedirs = (this.statedirs != null ? this.statedirs : NB_STATEDIR_PATHS); + this.setStatePath(); + + arglist = nonincludes; + nonincludes = new LinkedList<>(); + + // Now that statdirs is settled, auto load argsfile if it is present + NBCLIArgsFile argsfile = new NBCLIArgsFile(); + argsfile.reserved(NBCLICommandParser.RESERVED_WORDS); + argsfile.preload("--argsfile-optional", ARGS_FILE_DEFAULT); + arglist = argsfile.process(arglist); + + // Parse all --argsfile... and other high level options + + while (arglist.peekFirst() != null) { + String word = arglist.peekFirst(); + if (word.startsWith("--") && word.contains("=")) { + String wordToSplit = arglist.removeFirst(); + String[] split = wordToSplit.split("=", 2); + arglist.offerFirst(split[1]); + arglist.offerFirst(split[0]); + continue; + } + + switch (word) { + // These options modify other options. They should be processed early. + case NBCLIArgsFile.ARGS_FILE: + case NBCLIArgsFile.ARGS_FILE_OPTIONAL: + case NBCLIArgsFile.ARGS_FILE_REQUIRED: + case NBCLIArgsFile.ARGS_PIN: + case NBCLIArgsFile.ARGS_UNPIN: + if (this.statepath == null) { + setStatePath(); + } arglist = argsfile.process(arglist); break; case ANNOTATE_EVENTS: @@ -198,19 +245,10 @@ public class NBCLIOptions { String toAnnotate = readWordOrThrow(arglist, "annotated events"); annotateEvents = toAnnotate.split("\\\\s*,\\\\s*"); break; -// case ANNOTATE_TO_GRAFANA: -// arglist.removeFirst(); -// grafanaEndpoint = readWordOrThrow(arglist,"grafana API endpoint"); -// break; case ANNOTATORS_CONFIG: arglist.removeFirst(); this.annotatorsConfig = readWordOrThrow(arglist, "annotators config"); break; - case INCLUDE: - arglist.removeFirst(); - String include = readWordOrThrow(arglist, "path to include"); - wantsToIncludePaths.add(include); - break; case REPORT_GRAPHITE_TO: arglist.removeFirst(); reportGraphiteTo = arglist.removeFirst(); @@ -245,13 +283,57 @@ public class NBCLIOptions { break; default: nonincludes.addLast(arglist.removeFirst()); - } - } + return nonincludes; } + private Path setStatePath() { + if (statePathAccesses.size() > 0) { + throw new BasicError("The statedir must be set before it is used by other\n" + + " options. If you want to change the statedir, be sure you do it before\n" + + " dependent options. These parameters were called before this --statedir:\n" + + statePathAccesses.stream().map(s -> "> " + s).collect(Collectors.joining("\n"))); + } + if (this.statepath != null) { + return this.statepath; + } + + List paths = Environment.INSTANCE.interpolate(":", statedirs); + Path selected = null; + + for (String pathName : paths) { + Path path = Path.of(pathName); + if (Files.exists(path)) { + if (Files.isDirectory(path)) { + selected = path; + break; + } else { + logger.warn("possible state dir path is not a directory: '" + path.toString() + "'"); + } + } + } + if (selected == null) { + selected = Path.of(paths.get(0)); + } + + if (!Files.exists(selected)) { + try { + Files.createDirectories( + selected, + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwx---")) + ); + } catch (IOException e) { + throw new BasicError("Could not create state directory at '" + selected.toString() + "': " + e.getMessage()); + } + } + + Environment.INSTANCE.put(NBSTATEDIR, selected.toString()); + + return selected; + } + private void parseAllOptions(String[] args) { LinkedList arglist = parseGlobalOptions(args); @@ -565,7 +647,7 @@ public class NBCLIOptions { spec.indicatorMode = IndicatorMode.logonly; } else if (this.getCommands().stream().anyMatch(cmd -> cmd.getCmdType().equals(Cmd.CmdType.script))) { logger.info("Command line includes script calls, so progress data on console is " + - "suppressed."); + "suppressed."); spec.indicatorMode = IndicatorMode.logonly; } } @@ -577,7 +659,7 @@ public class NBCLIOptions { configs.stream().map(LoggerConfig::getFilename).forEach(s -> { if (files.contains(s)) { logger.warn(s + " is included in " + configName + " more than once. It will only be included " + - "in the first matching config. Reorder your options if you need to control this."); + "in the first matching config. Reorder your options if you need to control this."); } files.add(s); }); @@ -688,8 +770,8 @@ public class NBCLIOptions { break; default: throw new RuntimeException( - LOG_HISTOGRAMS + - " options must be in either 'regex:filename:interval' or 'regex:filename' or 'filename' format" + LOG_HISTOGRAMS + + " options must be in either 'regex:filename:interval' or 'regex:filename' or 'filename' format" ); } } @@ -714,7 +796,7 @@ public class NBCLIOptions { switch (parts.length) { case 2: Unit.msFor(parts[1]).orElseThrow( - () -> new RuntimeException("Unable to parse progress indicator indicatorSpec '" + parts[1] + "'") + () -> new RuntimeException("Unable to parse progress indicator indicatorSpec '" + parts[1] + "'") ); progressSpec.intervalSpec = parts[1]; case 1: diff --git a/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java b/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java deleted file mode 100644 index 91bb05ecc..000000000 --- a/engine-cli/src/test/java/io/nosqlbench/engine/cli/ArgsFileTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.nosqlbench.engine.cli; - -import org.junit.Test; - -import java.util.LinkedList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ArgsFileTest { - - @Test - public void testLoadingArgs() { - LinkedList result; - ArgsFile argsFile = new ArgsFile(); - result = argsFile.process("--argsfile", "src/test/resources/argsfiles/nonextant.cli"); - assertThat(result).containsExactly(); - result = argsFile.process("--argsfile", "src/test/resources/argsfiles/alphagamma.cli"); - assertThat(result).containsExactly("alpha", "gamma"); - } - - @Test(expected = RuntimeException.class) - public void testLoadingMissingRequiredFails() { - LinkedList result; - ArgsFile argsFile = new ArgsFile(); - result = argsFile.process("--argsfile-required", "src/test/resources/argsfiles/nonextant.cli"); - } - - @Test - public void testLoadingInPlace() { - LinkedList result; - LinkedList commands = new LinkedList<>(List.of("--abc", "--def", "--argsfile", "src/test/resources" + - "/argsfiles/alphagamma.cli")); - ArgsFile argsFile = new ArgsFile().preload("--argsfile-optional", "src/test/resources/argsfiles/alphagamma" + - ".cli"); - result = argsFile.process(commands); - assertThat(result).containsExactly("alpha", "gamma", "--abc", "--def", "alpha", "gamma"); - } - -} \ No newline at end of file diff --git a/engine-cli/src/test/java/io/nosqlbench/engine/cli/NBCLIArgsFileTest.java b/engine-cli/src/test/java/io/nosqlbench/engine/cli/NBCLIArgsFileTest.java new file mode 100644 index 000000000..9b2b8f55b --- /dev/null +++ b/engine-cli/src/test/java/io/nosqlbench/engine/cli/NBCLIArgsFileTest.java @@ -0,0 +1,117 @@ +package io.nosqlbench.engine.cli; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.LinkedList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NBCLIArgsFileTest { + + @Test + public void testLoadingArgs() { + LinkedList result; + NBCLIArgsFile argsFile = new NBCLIArgsFile(); + result = argsFile.process("--argsfile", "src/test/resources/argsfiles/nonextant.cli"); + assertThat(result).containsExactly(); + result = argsFile.process("--argsfile", "src/test/resources/argsfiles/alphagamma.cli"); + assertThat(result).containsExactly("alpha", "gamma"); + } + + @Test + public void loadParamsWithEnv() { + NBCLIArgsFile argsfile = new NBCLIArgsFile(); + LinkedList result = argsfile.process("--argsfile-required", "src/test/resources/argsfiles/home_env.cli"); + System.out.println(result); + } + + @Test(expected = RuntimeException.class) + public void testLoadingMissingRequiredFails() { + LinkedList result; + NBCLIArgsFile argsFile = new NBCLIArgsFile(); + result = argsFile.process("--argsfile-required", "src/test/resources/argsfiles/nonextant.cli"); + } + + @Test + public void testLoadingInPlace() { + LinkedList result; + LinkedList commands = new LinkedList<>(List.of("--abc", "--def")); + + NBCLIArgsFile argsFile = new NBCLIArgsFile().preload("--argsfile-optional", "src/test/resources/argsfiles/alphagamma" + + ".cli"); + result = argsFile.process(commands); + assertThat(result).containsExactly("alpha", "gamma", "--abc", "--def"); + } + + @Test + public void testLinesToArgs() { + NBCLIArgsFile argsfile = new NBCLIArgsFile().reserved("reservedword"); + LinkedList args = argsfile.linesToArgs( + List.of("--optionname argument", "--optionname2", "--opt3 reservedword") + ); + assertThat(args).containsExactly("--optionname", "argument", "--optionname2", "--opt3", "reservedword"); + } + + @Test + public void testOptionPinning() { + Path tempFile = null; + try { + tempFile = Files.createTempFile( + "tmpfile", + "cli", + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-r--")) + ); + + // preloading is a way to have global defaults based on + // the presence of a default file + NBCLIArgsFile argsfile = new NBCLIArgsFile().preload( + "--argsfile-optional", tempFile.toAbsolutePath().toString() + ); + + LinkedList commandline; + + commandline = argsfile.process( + "--pin", "--option1", + "--pin", "--option1", + "--pin", "--option2", "arg2" + ); + + String filecontents; + filecontents = Files.readString(tempFile); + + // logging should indicate no changes + commandline = argsfile.process( + "--pin", "--option1", + "--pin", "--option1", + "--pin", "--option2", "arg2" + ); + + // unpinned options should be discarded + commandline = argsfile.process( + "--unpin", "--option1", + "--option4", "--option5" + ); + + assertThat(commandline).containsExactly( + "--option4", "--option5" + ); + + assertThat(filecontents).isEqualTo("--option1\n--option2 arg2\n"); + + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + Files.deleteIfExists(tempFile); + } catch (Exception ignored) { + } + } + } + + +} \ No newline at end of file