au.net.causal.maven.plugins.boxdb.vagrant.LocalVagrant Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of boxdb-maven-plugin Show documentation
Show all versions of boxdb-maven-plugin Show documentation
Maven plugin to start databases using Docker and VMs
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 extends BoxDefinition> 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 extends Plugin> 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 extends BoxUpdateStatus> 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 extends Plugin> 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 extends BoxUpdateStatus> 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 extends BoxDefinition> 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);
}
}
}