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

au.net.causal.maven.plugins.boxdb.vagrant.LocalVagrant Maven / Gradle / Ivy

package au.net.causal.maven.plugins.boxdb.vagrant;

import au.net.causal.maven.plugins.boxdb.vagrant.BoxUpdateStatus.State;
import com.google.common.annotations.VisibleForTesting;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Vagrant implementation that runs a local Vagrant process.
 */

//This is a good source for Vagrant messages to parse:
//https://github.com/hashicorp/vagrant/blob/master/templates/locales/en.yml

public class LocalVagrant implements Vagrant
{
    private final Commandline baseCommandLine;
    private final Log log;

    /**
     * @param baseCommandLine command line that has just the Vagrant executable, no parameters.
     * @param log log that will receive output from the process.
     */
    public LocalVagrant(Commandline baseCommandLine, Log log)
    {
        Objects.requireNonNull(baseCommandLine, "baseCommandLine == null");
        Objects.requireNonNull(log, "log == null");

        this.baseCommandLine = baseCommandLine;
        this.log = log;
    }

    /**
     * Convenience converter varargs to String array.
     */
    private static String[] args(String... args)
    {
        return args;
    }

    private void configureBaseEnvironment(Commandline commandLine, BaseOptions options)
    {
        options.getEnvironment().forEach(commandLine::addEnvironment);
    }

    private void configureEnvironment(Commandline commandLine, InstanceOptions options)
    {
        commandLine.setWorkingDirectory(options.getBaseDirectory().toFile());

        if (options.getVagrantFileName() != null)
            commandLine.addEnvironment("VAGRANT_VAGRANTFILE", options.getVagrantFileName());

        configureBaseEnvironment(commandLine, options);
    }

    /**
     * Runs a Vagrant command.
     *
     * @param commandLine the command line to execute.
     * @param timeout execution timeout.
     *
     * @return the standard output from the command.
     *
     * @throws VagrantException if an execution error occurs.
     */
    private String executeCommand(Commandline commandLine, Duration timeout)
    throws VagrantException
    {
        CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
        CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();

        try
        {
            int exitCode = CommandLineUtils.executeCommandLine(commandLine, out, err, Math.toIntExact(timeout.getSeconds()));
            if (exitCode != 0)
                throw new VagrantException("Vagrant error " + exitCode + ": " + err.getOutput());
        }
        catch (CommandLineException e)
        {
            throw new VagrantException("Error running Vagrant: " + e, e);
        }

        return out.getOutput();
    }

    /**
     * Runs a Vagrant command, concatenating the error output at the end of the standard output.
     *
     * @param commandLine command line to execute.
     * @param timeout execution timeout.
     *
     * @return cmobined output of both standard out and standard error.
     *
     * @throws VagrantException if an execution error occurs.
     */
    private String executeCommandWithErr(Commandline commandLine, Duration timeout)
    throws VagrantException
    {
        CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
        CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();

        try
        {
            int exitCode = CommandLineUtils.executeCommandLine(commandLine, out, err, Math.toIntExact(timeout.getSeconds()));
            if (exitCode != 0)
                throw new VagrantException("Vagrant error " + exitCode + ": " + err.getOutput());
        }
        catch (CommandLineException e)
        {
            throw new VagrantException("Error running Vagrant: " + e, e);
        }

        return out.getOutput() + err.getOutput();
    }

    private void executeCommandWithLogging(Commandline commandLine, Duration timeout)
    throws VagrantException
    {
        StreamConsumer out = new LoggingConsumer(log, "vagrant> ");
        StreamConsumer err = new LoggingConsumer(log, "vagrant> ");

        try
        {
            int exitCode = CommandLineUtils.executeCommandLine(commandLine, out, err, Math.toIntExact(timeout.getSeconds()));
            if (exitCode != 0)
                throw new VagrantException("Vagrant error " + exitCode + ".");
        }
        catch (CommandLineException e)
        {
            throw new VagrantException("Error running Vagrant: " + e, e);
        }
    }

    @Override
    public void up(UpOptions options) throws VagrantException
    {
        Commandline statusCommand = (Commandline)baseCommandLine.clone();
        statusCommand.addArguments(args("up", options.getBoxName()));
        configureEnvironment(statusCommand, options);
        executeCommandWithLogging(statusCommand, options.getTimeout());
    }

    @Override
    public void destroy(DestroyOptions options) throws VagrantException
    {
        Commandline statusCommand = (Commandline)baseCommandLine.clone();
        statusCommand.addArguments(args("destroy", "--force", options.getBoxName()));
        configureEnvironment(statusCommand, options);
        executeCommandWithLogging(statusCommand, options.getTimeout());
    }

    @Override
    public void halt(HaltOptions options) throws VagrantException
    {
        Commandline statusCommand = (Commandline)baseCommandLine.clone();
        statusCommand.addArguments(args("halt", options.getBoxName()));
        configureEnvironment(statusCommand, options);
        executeCommandWithLogging(statusCommand, options.getTimeout());
    }

    @Override
    public void provision(ProvisionOptions options) throws VagrantException
    {
        Commandline statusCommand = (Commandline)baseCommandLine.clone();
        statusCommand.addArguments(args("provision", options.getBoxName()));
        if (!options.getProvisioners().isEmpty())
        {
            statusCommand.addArguments(args("--provision-with"));
            statusCommand.addArguments(args(String.join(",", options.getProvisioners())));
        }
        configureEnvironment(statusCommand, options);
        executeCommandWithLogging(statusCommand, options.getTimeout());
    }

    @Override
    public BoxStatus status(StatusOptions options)
    throws VagrantException
    {
        Commandline statusCommand = (Commandline)baseCommandLine.clone();
        statusCommand.addArguments(args("status", options.getBoxName()));
        configureEnvironment(statusCommand, options);
        String output = executeCommand(statusCommand, options.getTimeout());
        return parseStatusOutput(output, options.getBoxName());
    }

    @Override
    public List boxList(BoxListOptions options) throws VagrantException
    {
        Commandline listCommand = (Commandline)baseCommandLine.clone();
        listCommand.addArguments(args("box", "list"));
        String output = executeCommand(listCommand, options.getTimeout());
        return parseBoxListOutput(output);
    }

    @Override
    public void boxAdd(BoxAddOptions options)
    throws VagrantException
    {
        Commandline addCommand = (Commandline)baseCommandLine.clone();
        configureBaseEnvironment(addCommand, options);
        addCommand.addArguments(args("box", "add", options.getBox().getName()));
        addCommand.addArguments(args("--provider", options.getBox().getProvider()));
        addCommand.addArguments(args("--box-version", options.getBox().getVersion()));
        if (options.isForce())
            addCommand.addArguments(args("--force"));

        executeCommandWithLogging(addCommand, options.getTimeout());
    }

    @Override
    public void boxRemove(BoxRemoveOptions options) throws VagrantException
    {
        Commandline removeCommand = (Commandline)baseCommandLine.clone();
        configureBaseEnvironment(removeCommand, options);
        removeCommand.addArguments(args("box", "remove", options.getBox().getName()));
        removeCommand.addArguments(args("--provider", options.getBox().getProvider()));
        removeCommand.addArguments(args("--box-version", options.getBox().getVersion()));
        if (options.isForce())
            removeCommand.addArguments(args("--force"));
        executeCommandWithLogging(removeCommand, options.getTimeout());
    }

    @Override
    public void boxUpdate(BoxUpdateOptions options)
    throws VagrantException
    {
        Commandline updateCommand = (Commandline)baseCommandLine.clone();
        configureBaseEnvironment(updateCommand, options);
        updateCommand.addArguments(args("box", "update", options.getBox().getName()));
        updateCommand.addArguments(args("--provider", options.getBox().getProvider()));
        if (options.isForce())
            updateCommand.addArguments(args("--force"));
        executeCommandWithLogging(updateCommand, options.getTimeout());
    }

    @Override
    public List pluginList(PluginListOptions options) throws VagrantException
    {
        Commandline listCommand = (Commandline)baseCommandLine.clone();
        listCommand.addArguments(args("plugin", "list"));
        String output = executeCommand(listCommand, options.getTimeout());
        return parsePluginListOutput(output);
    }

    @Override
    public void pluginInstall(PluginInstallOptions options)
    throws VagrantException
    {
        Commandline installCommand = (Commandline)baseCommandLine.clone();
        installCommand.addArguments(args("plugin", "install", options.getPluginName()));
        executeCommandWithLogging(installCommand, options.getTimeout());
    }

    @Override
    public List boxOutdated(BoxOutdatedOptions options)
    throws VagrantException
    {
        Commandline outdatedCommand = (Commandline)baseCommandLine.clone();
        outdatedCommand.addArguments(args("box", "outdated", "--global"));
        String output = executeCommandWithErr(outdatedCommand, options.getTimeout());
        return parseBoxOutdatedOutput(output);
    }

    @VisibleForTesting
    List parsePluginListOutput(String output)
    throws VagrantException
    {
        //e.g.
        /*
        vagrant-exec (0.5.2)
        vagrant-share (1.1.5, system)
        vagrant-vbguest (0.12.0)
        */

        List results = new ArrayList<>();
        for (String line : output.split("[\\r\\n]+"))
        {
            line = line.trim();
            if (!line.isEmpty())
                results.add(parsePluginListLine(line));
        }

        return results;
    }

    @VisibleForTesting
    Plugin parsePluginListLine(String line)
    throws VagrantException
    {
        //e.g.
        //vagrant-vbguest (0.12.0)
        String[] tokens = line.split("([\\s\\(\\)])+");
        if (tokens.length < 2)
            throw new VagrantException("Failed to parse plugin list: " + line);

        return new Plugin(tokens[0], joinRemainingTokens(tokens, 1, " "));
    }

    /**
     * Join remaining elements in an array.
     * e.g. joinRemainingTokens({"one", "two", "three"}, 1, " ") -> "two three"
     */
    private static String joinRemainingTokens(String[] elements, int fromIndex, String joiner)
    {
        StringBuilder buf = new StringBuilder();
        for (int i = fromIndex; i < elements.length; i++)
        {
            buf.append(elements[i]);
            if (i < (elements.length - 1))
                buf.append(joiner);
        }
        return buf.toString();
    }

    @VisibleForTesting
    List parseBoxOutdatedOutput(String output)
    throws VagrantException
    {
        //e.g.
        /*
         * 'ubuntu/zesty64' for 'virtualbox' (v20171219.0.0) is up to date
         * 'ubuntu/bionic64' for 'virtualbox' is outdated! Current: 20180809.0.0. Latest: 20190210.0.0
         * 'test/mybox' for 'virtualbox' wasn't added from a catalog, no version information
         * 'ubuntu/bionic64' for 'virtualbox': Error loading metadata: Could not resolve host: vagrantcloud.com
         * 'ubuntu/artful64' for 'virtualbox': Error loading metadata: The requested URL returned error: 404 Not Found
         */

        List results = new ArrayList<>();
        for (String line : output.split("[\\r\\n]+"))
        {
            line = line.trim();
            if (!line.isEmpty() && !isIgnorableLine(line))
                results.add(parseBoxOutdatedLine(line));
        }

        return results;
    }

    @VisibleForTesting
    BoxUpdateStatus parseBoxOutdatedLine(String line)
    throws VagrantException
    {
        //Examples to handle
        /*
         * 'ubuntu/zesty64' for 'virtualbox' (v20171219.0.0) is up to date
         * 'ubuntu/bionic64' for 'virtualbox' is outdated! Current: 20180809.0.0. Latest: 20190210.0.0
         * 'test/mybox' for 'virtualbox' wasn't added from a catalog, no version information
         * 'ubuntu/bionic64' for 'virtualbox': Error loading metadata: Could not resolve host: vagrantcloud.com
         * 'ubuntu/artful64' for 'virtualbox': Error loading metadata: The requested URL returned error: 404 Not Found
         */

        //Always starts with '* '' for ''
        Pattern parsePattern = Pattern.compile("^\\s*\\*?\\s*'([^']+)'\\s+for\\s+'([^']+)'(.*)$");
        Matcher m = parsePattern.matcher(line);
        if (!m.matches())
            throw new VagrantException("Could not parse outdated status line: " + line);

        String boxName = m.group(1);
        String provider = m.group(2);
        String remainder = m.group(3).trim();

        //Up-to-date?
        Pattern upToDatePattern = Pattern.compile("\\(([^)]+)\\).*up\\s+to\\s+date");
        Matcher upToDateMatcher = upToDatePattern.matcher(remainder);
        if (upToDateMatcher.find())
        {
            String version = upToDateMatcher.group(1);
            return new BoxUpdateStatus(boxName, provider, State.UP_TO_DATE, version, version, line);
        }

        //Outdated?
        Pattern isOutdatedPattern = Pattern.compile("is outdated", Pattern.CASE_INSENSITIVE);
        if (isOutdatedPattern.matcher(remainder).find())
        {
            //Current: 20180809.0.0.  (might have trailing '.' which we should trim off)
            Pattern currentVersionPattern = Pattern.compile("Current:\\s+([^\\s]+)", Pattern.CASE_INSENSITIVE);
            Matcher currentVersionMatcher = currentVersionPattern.matcher(remainder);
            if (!currentVersionMatcher.find())
                throw new VagrantException("Could not parse current version from outdated line: " + line);

            String localVersion = trimTrailingDot(currentVersionMatcher.group(1));

            Pattern latestVersionPattern = Pattern.compile("Latest:\\s+([^\\s]+)", Pattern.CASE_INSENSITIVE);
            Matcher latestVersionMatcher = latestVersionPattern.matcher(remainder);
            if (!latestVersionMatcher.find())
                throw new VagrantException("Could not parse latest version from outdated line: " + line);

            String remoteVersion = trimTrailingDot(latestVersionMatcher.group(1));

            return new BoxUpdateStatus(boxName, provider, State.OUTDATED, localVersion, remoteVersion, line);
        }

        //No version information?
        Pattern noVersionInfoPattern = Pattern.compile("wasn't added from a catalog, no version information", Pattern.CASE_INSENSITIVE);
        Matcher noVersionInfoMatcher = noVersionInfoPattern.matcher(remainder);
        if (noVersionInfoMatcher.find())
            return new BoxUpdateStatus(boxName, provider, State.NO_VERSION_INFORMATION, null, null, line);

        //Error?
        Pattern errorPattern = Pattern.compile("Error loading metadata:.+", Pattern.CASE_INSENSITIVE);
        Matcher errorMatcher = errorPattern.matcher(remainder);
        if (errorMatcher.find())
            return new BoxUpdateStatus(boxName, provider, State.ERROR, null, null, line);

        //Don't know what it could be
        throw new VagrantException("Could not parse outdated status line: " + line);
    }

    private static String trimTrailingDot(String s)
    {
        if (s.endsWith("."))
            s = s.substring(0, s.length() - 1);

        return s;
    }

    @VisibleForTesting
    List parseBoxListOutput(String output)
    throws VagrantException
    {
        //e.g.
        /*
        debian/jessie64                 (virtualbox, 8.3.0)
        msabramo/mssqlserver2014express (virtualbox, 0.1)
        */

        //Special case handling for no boxes
        if (output.contains("There are no installed boxes! Use `vagrant box add` to add some."))
            return Collections.emptyList();

        List results = new ArrayList<>();
        for (String line : output.split("[\\r\\n]+"))
        {
            line = line.trim();
            if (!line.isEmpty() && !isIgnorableLine(line))
                results.add(parseBoxListLine(line));
        }

        return results;
    }

    private boolean isIgnorableLine(String line)
    {
        if (line.trim().contains("A new version of Vagrant is available:"))
            return true;

        return false;
    }

    @VisibleForTesting
    BoxDefinition parseBoxListLine(String line)
    throws VagrantException
    {
        //e.g.
        //debian/jessie64                 (virtualbox, 8.3.0)
        String[] tokens = line.split("([\\s\\(\\),])+");
        if (tokens.length != 3)
            throw new VagrantException("Failed to parse box list: " + line);

        return new BoxDefinition(tokens[0], tokens[2], tokens[1]);
    }

    @VisibleForTesting
    BoxStatus parseStatusOutput(String output, String boxName)
    throws VagrantException
    {
        /*
        Current machine states:

        boxdb-sqlserver           not created (virtualbox)

        The environment has not yet been created. Run `vagrant up` to
        ...
        */

        Pattern headerLinePattern = Pattern.compile(".*:$");

        int blankLineCount = 0;
        int headerLineCount = 0;
        //Ignore first line with text
        try (BufferedReader r = new BufferedReader(new StringReader(output)))
        {
            String line;
            do
            {
                line = r.readLine();
                if (line != null)
                {
                    if (line.trim().isEmpty())
                        blankLineCount++;
                    else if (headerLinePattern.matcher(line).matches())
                        headerLineCount++;
                    else if (headerLineCount > 0 && blankLineCount > 0)
                    {
                        if (line.startsWith(boxName))
                            return parseStatusLine(line);
                    }
                }
            }
            while (line != null);
        }
        catch (IOException e)
        {
            //Should not happen, all in memory
            throw new VagrantException(e);
        }

        //If we get here we have no status
        throw new VagrantException("Could not find status line for box '" + boxName + "' in output: " + output);
    }

    BoxStatus parseStatusLine(String line)
    throws VagrantException
    {
        //box           not created (virtualbox)
        //box           poweroff (virtualbox)
        //box           running (virtualbox)

        Pattern statusLinePattern = Pattern.compile("\\S+\\s+([^\\(]+)");
        Matcher matcher = statusLinePattern.matcher(line);
        boolean matched = matcher.find();
        if (!matched)
            throw new VagrantException("Failed to parse status: " + line);

        String statusText = matcher.group(1).trim();
        switch (statusText)
        {
            case "not created":
                return BoxStatus.NOT_CREATED;
            case "poweroff":
                return BoxStatus.STOPPED;
            case "running":
                return BoxStatus.RUNNING;
            case "aborted":
                return BoxStatus.ABORTED;
            case "stopping":
                return BoxStatus.STOPPING;
            case "paused":
                return BoxStatus.PAUSED;
            default:
                throw new VagrantException("Unknown status: " + line);
        }
    }

    /**
     * Every line of output from the Vagrant process is sent to the log at INFO level.
     */
    private static class LoggingConsumer implements StreamConsumer
    {
        private final Log log;
        private final String prefix;

        public LoggingConsumer(Log log, String prefix)
        {
            this.log = log;
            this.prefix = prefix;
        }

        @Override
        public void consumeLine(String line)
        {
            log.info(prefix + line);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy