All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.hyperfoil.tools.qdup.QDup Maven / Gradle / Ivy

Go to download

Coordinate multiple terminal shell connections for queuing performance tests and collecting output files

There is a newer version: 0.8.3
Show newest version
package io.hyperfoil.tools.qdup;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import io.hyperfoil.tools.qdup.cmd.*;
import io.hyperfoil.tools.qdup.config.RunConfigBuilder;
import io.hyperfoil.tools.qdup.config.RunError;
import org.apache.commons.cli.*;
import org.slf4j.LoggerFactory;
import org.slf4j.ext.XLogger;
import org.slf4j.ext.XLoggerFactory;
import io.hyperfoil.tools.qdup.config.RunConfig;
import io.hyperfoil.tools.qdup.config.yaml.Parser;
import io.hyperfoil.tools.qdup.config.yaml.YamlFile;
import io.hyperfoil.tools.yaup.AsciiArt;
import io.hyperfoil.tools.yaup.StringUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class QDup {

    private static final XLogger logger = XLoggerFactory.getXLogger(MethodHandles.lookup().lookupClass());
    private final String knownHosts;
    private final String identity;
    private final String passphrase;
    private final int timeout;
    private final Properties stateProps;
    private final Properties removeStateProperties;
    private final String trace;
    private final String traceName;

    private String outputPath;
    private String version;
    private String hash;

    private List yamlPaths;
    private int scheduledThreads;
    private int commandThreads;
    private boolean test;
    private List breakpoints;
    private boolean colorTerminal;
    private int jsonPort;

    private List skipStages;

    private boolean exitCode = false;

    private RunConfig config;

    public boolean checkExitCode(){return exitCode;}

    public boolean isColorTerminal() {
        return colorTerminal;
    }

    public List getBreakpoints() {
        return breakpoints;
    }

    public List getYamlPaths() {
        return yamlPaths;
    }

    public int getScheduledThreads() {
        return scheduledThreads;
    }

    public int getCommandThreads() {
        return commandThreads;
    }

    public String getKnownHosts() {
        return knownHosts;
    }

    public String getIdentity() {
        return identity;
    }

    public String getPassphrase() {
        return passphrase;
    }

    public int getTimeout() {
        return timeout;
    }

    public Properties getStateProps() {
        return stateProps;
    }

    public Properties getRemoveStateProperties() {
        return removeStateProperties;
    }

    public String getTrace() {
        return trace;
    }

    public boolean hasTrace() {
        return trace != null && !trace.isBlank();
    }

    public boolean isTest() {
        return test;
    }

    public String getOutputPath() {
        return outputPath;
    }

    public int getJsonPort() {
        return jsonPort;
    }

    public String getVersion() {
        return version;
    }

    public String getHash() {
        return hash;
    }

    public boolean hasSkipStages(){
        return !skipStages.isEmpty();
    }

    public List getSkipStages(){
        return skipStages;
    }

    public String getRunDebug(){ return config == null ? null : config.debug(true); };

    public QDup(String... args) {
        Options options = new Options();

        OptionGroup basePathGroup = new OptionGroup();
        basePathGroup.addOption(Option.builder("T")
                .longOpt("test")
                .required()
                .desc("test the yaml without running it")
                .build()
        );
        basePathGroup.addOption(Option.builder("b")
                .longOpt("basePath")
                .required()
                .hasArg()
                .argName("path")
                .desc("base path for the output folder, creates a new YYYYMMDD_HHmmss sub-folder")
                .build()
        );
        basePathGroup.addOption(Option.builder("B")
                .longOpt("fullPath")
                .required()
                .hasArg()
                .argName("path")
                .desc("full path for the output folder, does not create a sub-folder")
                .build()
        );

        basePathGroup.setRequired(false);

        options.addOptionGroup(basePathGroup);

        options.addOption(
                Option.builder("c")
                        .longOpt("commandPool")
                        .hasArg()
                        .argName("count")
                        .type(Integer.TYPE)
                        .desc("number of threads for executing commands [24]")
                        .build()
        );

        options.addOption(
                Option.builder("t")
                        .longOpt("timeout")
                        .hasArg()
                        .argName("seconds")
                        .type(Integer.TYPE)
                        .desc("session connection timeout in seconds, default 5s")
                        .build()
        );

        options.addOption(
                Option.builder("C")
                        .longOpt("colorTerminal")
                        .hasArg(false)
                        .desc("flag to enable color formatted terminal")
                        .build()
        );
        options.addOption(
                Option.builder("K")
                        .longOpt("breakpoint")
                        .hasArg(true)
                        .argName("breakpoint")
                        .desc("break before starting commands matching the pattern")
                        .build()
        );

        options.addOption(
                Option.builder("s")
                        .longOpt("scheduledPool")
                        .hasArg()
                        .argName("count")
                        .type(Integer.TYPE)
                        .desc("number of threads for executing scheduled tasks [4]")
                        .build()
        );

        options.addOption(
                Option.builder("S")
                        .argName("KEY=VALUE")
                        .desc("set a state parameter")
                        .hasArgs()
                        .numberOfArgs(2)
                        .valueSeparator()
                        .build()
        );

        options.addOption(
                Option.builder("SX")
                        .argName("KEY")
                        .desc("remove a state parameter")
                        .hasArg()
                        .build()
        );

        options.addOption(
                Option.builder("k")
                        .longOpt("knownHosts")
                        .desc("qdup known hosts path [~/.ssh/known_hosts]")
                        .hasArg()
                        .argName("path")
                        .type(String.class)
                        .build()
        );

        options.addOption(
                Option.builder("p")
                        .longOpt("passphrase")
                        .desc("qdup passphrase for identify file [null]")
                        .hasArg()
                        .optionalArg(true)
                        .argName("password")
                        .type(String.class)
                        .build()
        );
        options.addOption(
                Option.builder("i")
                        .longOpt("identity")
                        .argName("path")
                        .hasArg()
                        .desc("qdup identity path [~/.ssh/id_rsa]")
                        .type(String.class)
                        .build()
        );

        options.addOption(
                Option.builder("j")
                        .longOpt("jsonport")
                        .argName("port")
                        .hasArg()
                        .desc("preferred port for json server [31337]")
                        .type(Integer.class)
                        .build()
        );

        //logging
        options.addOption(
                Option.builder("l")
                        .longOpt("logback")
                        .argName("path")
                        .hasArg()
                        .desc("logback configuration path")
                        .type(String.class)
                        .build()
        );

        options.addOption(
                Option.builder("R")
                        .longOpt("trace")
                        .argName("pattern")
                        .hasArg()
                        .desc("trace ssh sessions matching the pattern")
                        .type(String.class)
                        .build()
        );
        options.addOption(
                Option.builder("RN")
                        .longOpt("traceName")
                        .argName("pattern")
                        .hasArg()
                        .desc("unique ID pattern for trace files")
                        .type(String.class)
                        .build()
        );

        //exit code checking
        options.addOption(
                Option.builder("x")
                        .longOpt("exitCode")
                        .desc("flag to enable exit code checking")
                        .build()
        );

        //only run specific stages
        options.addOption(
                Option.builder()
                    .longOpt("skip-stages")
                    .hasArgs()
                    .argName("stage")
                    .valueSeparator(',')
                    .desc("only perform specific stages")
                    .build()
        );

        CommandLineParser parser = new DefaultParser();
        HelpFormatter formatter = new HelpFormatter();
        CommandLine commandLine = null;

        String cmdLineSyntax = "[options] [yamlFiles]";

        cmdLineSyntax =
                "java -jar " +
                        (new File(QDup.class
                                .getProtectionDomain()
                                .getCodeSource()
                                .getLocation()
                                .getPath()
                        )).getName() +
                        " " +
                        cmdLineSyntax;

        if (args.length == 0) {
            formatter.printHelp(cmdLineSyntax, options);
            System.exit(0);
        }

        try {
            commandLine = parser.parse(options, args);
        } catch (ParseException e) {
            logger.error(e.getMessage(), e);
            formatter.printHelp(cmdLineSyntax, options);
            System.exit(1);
        }

        //load a custom logback configuration
        if (commandLine.hasOption("logback")) {
            String configPath = commandLine.getOptionValue("logback");
            LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

            try {
                JoranConfigurator configurator = new JoranConfigurator();
                configurator.setContext(context);
                context.reset();
                configurator.doConfigure(configPath);
            } catch (JoranException je) {
                // StatusPrinter will handle this
            }
            StatusPrinter.printInCaseOfErrorsOrWarnings(context);
        }

        knownHosts = commandLine.getOptionValue("knownHosts", RunConfigBuilder.DEFAULT_KNOWN_HOSTS);
        identity = commandLine.getOptionValue("identity", RunConfigBuilder.DEFAULT_IDENTITY);
        passphrase = commandLine.getOptionValue("passphrase", RunConfigBuilder.DEFAULT_PASSPHRASE);
        timeout = Integer.parseInt(commandLine.getOptionValue("timeout", "" + RunConfigBuilder.DEFAULT_SSH_TIMEOUT));
        commandThreads = Integer.parseInt(commandLine.getOptionValue("commandPool", "24"));
        scheduledThreads = Integer.parseInt(commandLine.getOptionValue("scheduledPool", "24"));
        yamlPaths = commandLine.getArgList();
        stateProps = commandLine.getOptionProperties("S");
        removeStateProperties = commandLine.getOptionProperties("SX");
        test = commandLine.hasOption("test");
        breakpoints = commandLine.hasOption("breakpoint") ? Arrays.asList(commandLine.getOptionValues("breakpoint")) : Collections.EMPTY_LIST;
        colorTerminal = commandLine.hasOption("colorTerminal");
        jsonPort = Integer.parseInt(commandLine.getOptionValue("jsonport", "" + JsonServer.DEFAULT_PORT));

        exitCode = commandLine.hasOption("exitCode");

        outputPath = null;
        DateTimeFormatter dt = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
        String uid = dt.format(LocalDateTime.now());

        trace = commandLine.getOptionValue("trace", "");
        traceName = commandLine.getOptionValue("traceName",""+uid);

        skipStages = commandLine.hasOption("skip-stages") ? Arrays.asList(commandLine.getOptionValues("skip-stages")).stream().map(str->StringUtil.getEnum(str,Stage.class,Stage.Invalid)).collect(Collectors.toList()) : Collections.EMPTY_LIST;

        if (commandLine.hasOption("basePath")) {
            outputPath = commandLine.getOptionValue("basePath") + "/" + uid;
        } else if (commandLine.hasOption("fullPath")) {
            outputPath = commandLine.getOptionValue("fullPath");
        } else {
            outputPath = "/tmp/"+uid;
        }

        Properties properties = new Properties();
        try (InputStream is = QDup.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
            if (is != null) {
                properties.load(is);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (InputStream is = QDup.class.getResourceAsStream("/META-INF/maven/io.hyperfoil.tools/qDup/pom.properties")) {
            if (is != null) {
                properties.load(is);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        version = properties.getProperty("version", "unknown");
        hash = properties.getProperty("hash", "unknown");

        if (yamlPaths.isEmpty()) {
            logger.error("Missing required yaml file(s)");
            formatter.printHelp(cmdLineSyntax, options);
            System.exit(1);
            return;
        }
    }

    public static void main(String[] args) {
        new QDup(args).run();
    }

    public void run() {

        Parser yamlParser = Parser.getInstance();
        yamlParser.setAbortOnExitCode(checkExitCode());
        RunConfigBuilder runConfigBuilder = new RunConfigBuilder();

        for (String yamlPath : getYamlPaths()) {
            File yamlFile = new File(yamlPath);
            if (!yamlFile.exists()) {
                logger.error("Error: cannot find " + yamlPath);
                System.exit(1);//return error to shell / jenkins
            } else {
                if (yamlFile.isDirectory()) {
                    logger.trace("loading directory: " + yamlPath);
                    for (File child : yamlFile.listFiles()) {
                        if (child.getName().endsWith(".yaml") || child.getName().endsWith(".yml")) {
                            logger.trace("  loading: " + child.getPath());
                            //String content = FileUtility.readFile(child.getPath());
                            YamlFile file = yamlParser.loadFile(child.getPath());
                            if (file == null) {
                                logger.error("Aborting run due to error reading {}", child.getPath());
                                System.exit(1);
                            }
                            runConfigBuilder.loadYaml(file);
                        } else {
                            logger.trace("  skipping: " + child.getPath());
                        }
                    }
                } else {
                    logger.trace("loading: " + yamlPath);
                    YamlFile file = yamlParser.loadFile(yamlPath);
                    if (file == null) {
                        logger.error("Aborting run due to error reading {}", yamlPath);
                        System.exit(1);
                    }
                    runConfigBuilder.loadYaml(file);

                }
            }
        }


        if (!getStateProps().isEmpty()) {
            getStateProps().forEach((k, v) -> {
                if (v != null && !v.toString().trim().isEmpty()) {
                    runConfigBuilder.forceRunState(k.toString(), v.toString());
                }
            });
        }


        if (!getRemoveStateProperties().isEmpty()) {
            getRemoveStateProperties().forEach((k, v) -> {
                if (k != null) {
                    runConfigBuilder.forceRunState(k.toString(), "");
                }
            });
        }
        if (hasTrace()) {
            runConfigBuilder.trace(getTrace());
        }

        if (getIdentity() != RunConfigBuilder.DEFAULT_IDENTITY) {
            runConfigBuilder.setIdentity(getIdentity());
        }

        if (getPassphrase() != RunConfigBuilder.DEFAULT_PASSPHRASE) {
            runConfigBuilder.setPassphrase(getPassphrase());
        }

        if (getKnownHosts() != RunConfigBuilder.DEFAULT_KNOWN_HOSTS) {
            runConfigBuilder.setKnownHosts(getKnownHosts());
        }

        if (hasSkipStages()){
            getSkipStages().forEach(runConfigBuilder::addSkipStage);
        }

        config = runConfigBuilder.buildConfig(yamlParser);
        if (isTest()) {
            //logger.info(config.debug());
            System.out.printf("%s", getRunDebug());
            System.exit(0);
        }

        File outputFile = new File(getOutputPath());
        if (!outputFile.exists()) {
            outputFile.mkdirs();
        }

        //TODO RunConfig should be immutable and terminal color is probably better stored in Run
        //TODO should we separte yaml config from environment config (identity, knownHosts, threads, color terminal)
        config.setColorTerminal(isColorTerminal());

        if (config.hasErrors()) {
            config.getErrors().stream().map(RunError::toString).forEach(error -> {
                System.out.printf("%s%n", error);
            });
            System.exit(1);
            return;
        }


        if(hasTrace()){
            config.getSettings().set(RunConfig.TRACE_NAME, traceName);
        }

        final AtomicInteger factoryCounter = new AtomicInteger(0);
        final AtomicInteger scheduledCounter = new AtomicInteger(0);

        BlockingQueue workQueue = new LinkedBlockingQueue<>();

        final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (thread, throwable) -> {

            logger.error("UNCAUGHT:" + thread.getName() + " " + throwable.getMessage(), throwable);
        };

        ThreadFactory factory = r -> {
            Thread rtrn = new Thread(r, "qdup-command-" + factoryCounter.getAndIncrement());
            rtrn.setUncaughtExceptionHandler(uncaughtExceptionHandler);
            return rtrn;
        };
        ThreadPoolExecutor executor = new ThreadPoolExecutor(getCommandThreads() / 2, getCommandThreads(), 30, TimeUnit.MINUTES, workQueue, factory);

        ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(getScheduledThreads(), runnable -> new Thread(runnable, "qdup-scheduled-" + scheduledCounter.getAndIncrement()));

        Dispatcher dispatcher = new Dispatcher(executor, scheduled);

        if (!getBreakpoints().isEmpty()) {
            Scanner scanner = new Scanner(System.in);
            getBreakpoints().forEach(breakpoint -> {
                dispatcher.addContextObserver(new ContextObserver() {
                    @Override
                    public void preStart(Context context, Cmd command) {
                        String commandString = command.toString();
                        boolean matches = commandString.contains(breakpoint) || commandString.matches(breakpoint);
                        if (matches) {
                            System.out.printf(
                                    "%sBREAKPOINT starting command%s%n" +
                                            "  breakpoint: %s%n" +
                                            "  command: %s%n" +
                                            "  script: %s%n" +
                                            "  host: %s%n" +
                                            "  context: %s%n" +
                                            "Press enter to continue:",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RED : "",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RESET : "",
                                    breakpoint,
                                    command.toString(),
                                    command.getHead().toString(),
                                    context.getHost().toString(),
                                    context instanceof ScriptContext ? ((ScriptContext)context).getContextId() : ""
                            );
                            String line = scanner.nextLine();
                        }
                    }
                    @Override
                    public void preNext(Context context, Cmd command, String output){
                        String commandString = command.toString();
                        boolean matches = commandString.contains(breakpoint) || commandString.matches(breakpoint);
                        if (matches) {
                            System.out.printf(
                                    "%sBREAKPOINT after command calls next%s%n" +
                                            "  breakpoint: %s%n" +
                                            "  command: %s%n" +
                                            "  script: %s%n" +
                                            "  host: %s%n" +
                                            "  context: %s%n" +
                                            "Press enter to continue:",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RED : "",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RESET : "",
                                    breakpoint,
                                    command.toString(),
                                    command.getHead().toString(),
                                    context.getHost().toString(),
                                    context instanceof ScriptContext ? ((ScriptContext)context).getContextId() : ""
                            );
                            String line = scanner.nextLine();
                        }
                    }
                    @Override
                    public void preSkip(Context context, Cmd command, String output){
                        String commandString = command.toString();
                        boolean matches = commandString.contains(breakpoint) || commandString.matches(breakpoint);
                        if (matches) {
                            System.out.printf(
                                    "%sBREAKPOINT after command calls skip%s%n" +
                                            "  breakpoint: %s%n" +
                                            "  command: %s%n" +
                                            "  script: %s%n" +
                                            "  host: %s%n" +
                                            "  context: %s%n" +
                                            "Press enter to continue:",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RED : "",
                                    config.isColorTerminal() ? AsciiArt.ANSI_RESET : "",
                                    breakpoint,
                                    command.toString(),
                                    command.getHead().toString(),
                                    context.getHost().toString(),
                                    context instanceof ScriptContext ? ((ScriptContext)context).getContextId() : ""
                            );
                            String line = scanner.nextLine();
                        }
                    }
                });

            });
        }

        config.getSettings().set("check-exit-code", checkExitCode());

        final Run run = new Run(getOutputPath(), config, dispatcher);

        logger.info("Starting with output path = " + run.getOutputPath());

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (!run.isAborted()) {
                run.abort(false);
                run.writeRunJson();
            }
        }, "shutdown-abort"));

        JsonServer jsonServer = new JsonServer(run, getJsonPort());

        jsonServer.start();

        long start = System.currentTimeMillis();
        run.getRunLogger().info("Running qDup version {} @ {}", getVersion(), getHash());
        run.run();
        long stop = System.currentTimeMillis();
        System.out.printf("Finished in %s at %s%n", StringUtil.durationToString(stop - start), run.getOutputPath());
        jsonServer.stop();
        dispatcher.shutdown();
        executor.shutdownNow();
        scheduled.shutdownNow();
        if (run.isAborted()) {
            System.exit(1);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy