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

au.net.causal.maven.plugins.boxdb.db.Db2Database Maven / Gradle / Ivy

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 logFiles() throws BoxDatabaseException, IOException
    {
        return logFilesInContainer(true, StandardCharsets.UTF_8, "/database/config/db2inst1/sqllib/db2dump/DIAG0000/",
                "db2diag.log");
    }

    @Override
    public Collection 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;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy