au.net.causal.maven.plugins.boxdb.db.Db2Database 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.db;
import au.net.causal.maven.plugins.boxdb.ImageCheckerUtils;
import au.net.causal.maven.plugins.boxdb.JdbcSqlRunner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.fabric8.maven.docker.access.DockerAccess;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.config.LogConfiguration;
import io.fabric8.maven.docker.config.RunImageConfiguration;
import io.fabric8.maven.docker.config.RunVolumeConfiguration;
import io.fabric8.maven.docker.log.LogOutputSpec;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.archiver.util.DefaultFileSet;
import org.codehaus.plexus.util.FileUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class Db2Database extends DockerDatabase
{
private final Path hostScriptDirectory;
public Db2Database(BoxConfiguration boxConfiguration, ProjectConfiguration projectConfiguration, BoxContext context,
DockerRegistry dockerRegistry)
throws IOException
{
super(boxConfiguration, projectConfiguration, context, dockerRegistry);
//Generate host script directory that is used to give scripts to DB2
hostScriptDirectory = context.getTempDirectory().resolve(containerName() + "-scripts");
if (Files.notExists(hostScriptDirectory))
Files.createDirectories(hostScriptDirectory);
}
@Override
protected String dockerImageName()
{
return "ibmcom/db2";
}
@Override
protected void configureRunImage(RunImageConfiguration.Builder builder)
{
super.configureRunImage(builder);
RunVolumeConfiguration scriptVolume = new RunVolumeConfiguration.Builder()
.bind(Arrays.asList(hostScriptDirectory.toAbsolutePath().toString() + ":/data/scripts"))
.build();
builder.env(ImmutableMap.of("DB2INST1_PASSWORD", getBoxConfiguration().getAdminPassword(),
"LICENSE", "accept"));
builder.volumes(scriptVolume);
//This is instead of running in privileged mode - seems like DB2 needs these two extra capabilities
//Not ideal, but better than giving it everything with privileged=true
builder.capAdd(ImmutableList.of("IPC_LOCK", "IPC_OWNER"));
}
@Override
protected int containerDatabasePort()
{
return 50000;
}
@Override
public void configureNewDatabase()
throws IOException, SQLException, BoxDatabaseException
{
//Add user through OS
createOsUser(getBoxConfiguration().getDatabaseUser(), getBoxConfiguration().getDatabasePassword());
String territory = getBoxConfiguration().getConfiguration().get(Db2Factory.DB2_TERRITORY_PROPERTY);
String collation = getBoxConfiguration().getConfiguration().get(Db2Factory.DB2_COLLATION_PROPERTY);
//Create database
getContext().getLog().info("Creating DB2 database '" + getBoxConfiguration().getDatabaseName() + "'...");
String init = "create database " + getBoxConfiguration().getDatabaseName() +
" using codeset " + getBoxConfiguration().getDatabaseEncoding() +
" territory " + territory +
" collate using " + collation;
executeDb2(init, getBoxConfiguration().getAdminUser());
}
protected String db2HomePath()
{
return "/opt/ibm/db2/V11.5/bin/";
}
protected String db2InstanceName()
{
return "db2inst1";
}
private int executeOsCommand(String commandLine, String label, boolean silent, Path returnCodeFile)
throws BoxDatabaseException, IOException
{
DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();
LogConfiguration logConfig = new LogConfiguration.Builder()
.enabled(!silent)
.prefix(label)
.build();
RunImageConfiguration commandRunConfig = new RunImageConfiguration.Builder()
.links(Collections.singletonList(containerName() + ":" + label))
.containerNamePattern("%a")
.cmd("unused") //not used but must be non-null - is replaced later
.log(logConfig)
.build();
commandRunConfig.getCmd().setExec(Arrays.asList("bash", "-i", "-c", commandLine + " ; echo $? > " + "/data/scripts/" + returnCodeFile.getFileName().toString()));
commandRunConfig.getCmd().setShell(null);
ImageConfiguration commandConfig = new ImageConfiguration.Builder()
.runConfig(commandRunConfig)
.name(dockerImageName())
.alias(containerName() + "-" + label)
.build();
String containerId = findDockerContainer().getId();
String execContainerId = docker.createExecContainer(containerId, commandRunConfig.getCmd());
LogOutputSpec execLog;
Path logFile = null;
if (silent)
{
logFile = Files.createTempFile(returnCodeFile.getParent(), "wait", ".log");
execLog = new LogOutputSpec.Builder().logStdout(false).file(logFile.toAbsolutePath().toString()).build();
}
else
execLog = getContext().getLogSpecFactory().createSpec(execContainerId, commandConfig);
docker.startExecContainer(execContainerId, execLog);
getContext().getLog().debug("Exec container done " + execContainerId);
String exitCode = new String(Files.readAllBytes(returnCodeFile), StandardCharsets.UTF_8).trim();
getContext().getLog().debug("All done: " + exitCode);
//If we wanted silent run we won't want to keep the output so just delete these files
//otherwise we'll have a huge amount of them, especially for the waiter script
if (logFile != null)
{
if (getContext().getLog().isDebugEnabled())
getContext().getLog().debug(Files.lines(logFile).collect(Collectors.joining(System.lineSeparator())));
try
{
Files.deleteIfExists(logFile);
}
catch (IOException e)
{
//Not a huge deal if we can't delete
getContext().getLog().debug("Failed to delete temporary log file " + logFile + ": " + e, e);
}
}
return Integer.parseInt(exitCode);
}
private void executeDb2(String args, String user)
throws BoxDatabaseException, IOException
{
//Docker plugin provides no easy way to get exit code from exec call so we need to script it and
//return its value via the file system and a temp file
Path returnCodeFile = Files.createTempFile(hostScriptDirectory, "return", ".txt");
executeDb2(args, returnCodeFile, false, user);
}
private void executeDb2(String args, Path returnCodeFile, boolean silent, String user)
throws BoxDatabaseException, IOException
{
int exitCode = executeOsCommand("su - " + user + " -c \"DB2INSTANCE=" + db2InstanceName() + " " + db2HomePath() + "db2 " + args + "\"",
"db2", silent, returnCodeFile);
if (exitCode != 0)
throw new Db2ExecutionException("DB2 tool exit code: " + exitCode, exitCode);
}
private void createOsUser(String user, String password)
throws BoxDatabaseException, IOException
{
//Docker plugin provides no easy way to get exit code from exec call so we need to script it and
//return its value via the file system and a temp file
Path returnCodeFile = Files.createTempFile(hostScriptDirectory, "return", ".txt");
int exitCode = executeOsCommand("useradd " + user, "useradd", false, returnCodeFile);
if (exitCode != 0)
throw new BoxDatabaseException("Error adding user " + user + ": " + exitCode);
exitCode = executeOsCommand("echo " + ScriptUtils.shellEscape(password) + " | passwd " + user + " --stdin",
"passwd", false, returnCodeFile);
if (exitCode != 0)
throw new BoxDatabaseException("Error setting password for user " + user + ": " + exitCode);
}
@Override
public JdbcConnectionInfo jdbcConnectionInfo(DatabaseTarget target)
throws BoxDatabaseException
{
String databaseName = getBoxConfiguration().getDatabaseName();
String uri = "jdbc:db2://" +
getContext().getDockerHostAddress() +
":" + getBoxConfiguration().getDatabasePort() +
"/" + databaseName;
return new JdbcConnectionInfo(uri,
target.user(getBoxConfiguration()),
target.password(getBoxConfiguration()),
getContext().getDockerHostAddress(),
getBoxConfiguration().getDatabasePort());
}
@Override
public JdbcDriverInfo jdbcDriverInfo() throws BoxDatabaseException
{
return new JdbcDriverInfo(new RunnerDependency("com.ibm.db2", "jcc", "11.5.0.0"), "com.ibm.db2.jcc.DB2Driver");
}
protected DataSourceBuilder dataSourceBuilder(DatabaseTarget target)
throws BoxDatabaseException
{
JdbcConnectionInfo jdbcInfo = jdbcConnectionInfo(target);
return new DataSourceBuilder(getContext())
.dataSourceClassName("com.ibm.db2.jcc.DB2SimpleDataSource")
.dependencies(jdbcDriverInfo().getDependencies())
.configureDataSource("setServerName", String.class, jdbcInfo.getHost())
.configureDataSource("setPortNumber", int.class, jdbcInfo.getPort())
.configureDataSource("setDriverType", int.class, 4) //JDBC type 4 driver
.configureDataSource("setDatabaseName", String.class, getBoxConfiguration().getDatabaseName())
.configureDataSource("setUser", String.class, jdbcInfo.getUser())
.configureDataSource("setPassword", String.class, jdbcInfo.getPassword());
}
@Override
public Connection createJdbcConnection(DatabaseTarget targetDatabase)
throws SQLException, BoxDatabaseException, IOException
{
return dataSourceBuilder(targetDatabase).create().getConnection();
}
@Override
protected void executeScriptFile(Path scriptFile, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
if (!Files.exists(scriptFile))
throw new NoSuchFileException(scriptFile.toString());
//Copy the script file to inside our script dir mounted to the container
Path mountedScriptFile = Files.createTempFile(hostScriptDirectory, "script", ".sql");
Files.copy(scriptFile, mountedScriptFile, StandardCopyOption.REPLACE_EXISTING);
executeDb2("-svtf " + "/data/scripts/" + mountedScriptFile.getFileName().toString(), targetDatabase.user(getBoxConfiguration()));
}
@Override
public void executeSql(String sql, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
executeDb2("-svt " + sql, targetDatabase.user(getBoxConfiguration()));
}
@Override
public void executeJdbcScript(Reader scriptReader, DatabaseTarget targetDatabase)
throws IOException, SQLException, BoxDatabaseException
{
try (Connection con = createJdbcConnection(targetDatabase);
JdbcSqlRunner sqlRunner = new JdbcSqlRunner(con, getContext().getLog()))
{
sqlRunner.executeSql(new BufferedReader(scriptReader));
}
}
@Override
public void backup(Path backupFile, BackupFileTypeHint backupFileTypeHint)
throws BoxDatabaseException, IOException, SQLException
{
//This is the backup directory created by DB2
String db2BackupFileName = backupFile.getFileName().toString();
Path targetBackupDirectory = hostScriptDirectory.resolve(db2BackupFileName);
Files.createDirectories(targetBackupDirectory);
//Quiesce DB to force other users off
executeSql("quiesce instance " + db2InstanceName() + " immediate force connections;", DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
//Run the backup
String sqlCommand = "backup database " + getBoxConfiguration().getDatabaseName() +
" to /data/scripts/" + targetBackupDirectory.getFileName().toString() +
" without prompting;";
executeSql(sqlCommand, DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
//Tar the directory up into target file
try
{
Archiver archiver = getContext().getArchiverManager().getArchiver("tar");
//TODO should we allow the archiver to be configured by configuration?
archiver.setDestFile(backupFile.toFile());
archiver.addFileSet(new DefaultFileSet(targetBackupDirectory.toFile()));
archiver.createArchive();
}
catch (NoSuchArchiverException e)
{
throw new BoxDatabaseException("Could not find archiver: " + e, e);
}
//Delete the directory when we're done
FileUtils.deleteDirectory(targetBackupDirectory.toFile());
//Unquiesce DB
executeSql("unquiesce instance " + db2InstanceName() + ";", DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
}
@Override
public void restore(Path backupFile)
throws BoxDatabaseException, IOException, SQLException
{
//Drop the existing database
executeSql("drop database " + getBoxConfiguration().getDatabaseName() + ";",
DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
//This is the backup directory created by DB2
String db2BackupFileName = backupFile.getFileName().toString();
Path targetBackupDirectory = hostScriptDirectory.resolve(db2BackupFileName);
FileUtils.deleteDirectory(targetBackupDirectory.toFile());
Files.createDirectories(targetBackupDirectory);
try
{
UnArchiver unArchiver = getContext().getArchiverManager().getUnArchiver("tar");
unArchiver.setDestDirectory(targetBackupDirectory.toFile());
unArchiver.setSourceFile(backupFile.toFile());
unArchiver.extract();
}
catch (NoSuchArchiverException e)
{
throw new BoxDatabaseException("Could not find unarchiver: " + e, e);
}
//Now run the restore command
executeSql("restore database " + getBoxConfiguration().getDatabaseName() +
" from " + "/data/scripts/" + targetBackupDirectory.getFileName().toString() +
" replace existing without prompting;",
DatabaseTarget.ADMIN, getProjectConfiguration().getBackupTimeout());
}
@Override
public boolean databaseIsReady(SQLException ex)
{
//For database connections that are not non-transient connection errors and have specific error codes
//(typically required parameter is not set error or database does not exist or other error due to missing DB)
//it means we got through to DB2 - database might not exist because it hasn't been created yet
//or we are attempting to connect without database configured (purely to test connectivity)
if (!(ex instanceof SQLNonTransientConnectionException) &&
(ex.getErrorCode() == -4462 || ex.getErrorCode() == -4228))
{
return true; //Observed to happen after initial connection succeeds
}
//This one can also happen with later JDBC drivers and means the database doesn't exist yet
if (!(ex instanceof SQLNonTransientConnectionException) &&
"58031".equals(ex.getSQLState()) && ex.getErrorCode() == -1031)
{
return true; //Observed to happen after initial connection succeeds
}
return false;
}
@Override
public List extends DatabaseLog> logFiles() throws BoxDatabaseException, IOException
{
return logFilesInContainer(true, StandardCharsets.UTF_8, "/database/config/db2inst1/sqllib/db2dump/DIAG0000/",
"db2diag.log");
}
@Override
public Collection extends ImageComponent> checkImage()
throws BoxDatabaseException
{
ImageComponent jdbcDriverComponent = ImageCheckerUtils.checkImageUsingMavenDependencies("JDBC driver",
getContext(),
jdbcDriverInfo().getDependencies());
ImageComponent dockerDatabaseComponent = checkDockerDatabaseImage(Db2Factory.DB2_DOCKER_REPOSITORY);
return ImmutableList.of(jdbcDriverComponent, dockerDatabaseComponent);
}
public static class Db2ExecutionException extends BoxDatabaseException
{
private final int exitCode;
public Db2ExecutionException(String reason, int exitCode)
{
super(reason);
this.exitCode = exitCode;
}
public int getExitCode()
{
return exitCode;
}
}
}