/* * Copyright (C) 2012 Intel Corporation * All rights reserved. */ package com.intel.mtwilson.setup.cmd; import com.intel.mtwilson.setup.PropertyHidingConfiguration; import com.intel.mountwilson.as.common.ASConfig; import com.intel.mtwilson.My; import com.intel.mtwilson.MyPersistenceManager; import com.intel.dcsg.cpg.io.Classpath; import com.intel.dcsg.cpg.jpa.PersistenceManager; import com.intel.dcsg.cpg.console.Command; import com.intel.mtwilson.setup.SetupContext; import com.intel.mtwilson.setup.SetupException; import com.intel.mtwilson.setup.SetupWizard; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URL; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.sql.DataSource; import org.apache.commons.configuration.Configuration; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Bug #509 create java program to handle database updates and ensure that * old updates (already executed) are not executed again * * Added -n (dry run) option on request from the SAM team: provide a way to determine if the * installer is compatible with the database; in other words, can the installer use the existing * database or does it know how to upgrade it as necessary. This is only true if the changelog * in the database is a subset of what is in the installer - in this case return a dry run success. * If the database has any entries that are not in the installer, return a dry run failure. * * Examples: * * java -jar setup-console-1.2-SNAPSHOT-with-dependencies.jar InitDatabase postgresql --check * * java -jar setup-console-1.2-SNAPSHOT-with-dependencies.jar InitDatabase mysql --check * * Test cases for the --check option: * 1. install mt wilson, then run the tool. Should report no changes are necessary * 2. delete some entries from the mw_changelog table, run the tool. Should report that there are database upgrades to apply. * 3. create new bogus entries in the mw_changelog table, run the tool. Should report that it's not compatible with the database * * References: * http://stackoverflow.com/questions/1429172/how-do-i-list-the-files-inside-a-jar-file * http://stackoverflow.com/questions/1044194/running-a-sql-script-using-mysql-with-jdbc * * @deprecated moving this functionality to InitDatabase in mtwilson-setup * @author jbuhacoff */ public class InitDatabase implements Command { private final Logger log = LoggerFactory.getLogger(getClass()); private String databaseVendor = null; private Configuration options = null; @Override public void setOptions(Configuration options) { this.options = options; } @Override public void execute(String[] args) throws Exception { // first arg: mysql or postgresql (installer detects and invokes this command with that argument) if( args.length < 1 ) { throw new SetupException("Usage: InitDatabase mysql|postgresql [--check]"); } databaseVendor = args[0]; vendor = databaseVendor; try { initDatabase(); } catch(Exception e) { throw new SetupException("Cannot setup database: "+e.toString(), e); } } public static class ChangelogEntry { public String id; public String applied_at; public String description; public ChangelogEntry() { } public ChangelogEntry(String id, String applied_at, String description) { this.id = id; this.applied_at = applied_at; this.description = description; } } private String vendor = null; /* private boolean checkDatabaseConnection() throws SetupException, IOException, SQLException { DataSource ds = getDataSourceNoSchema(); try { Connection c = ds.getConnection(); log.debug("Connected to database"); return true; } catch(SQLException e) { log.debug("Database connection failed: {}",e.toString(), e); log.error("Failed to connect to {} without schema", databaseVendor); return false; } } */ private void verbose(String format, Object... args) { if( options.getBoolean("verbose", false) ) { System.out.println(String.format(format, args)); } } private void initDatabase() throws SetupException, IOException, SQLException { log.debug("Loading SQL for {}", databaseVendor); Map<Long,Resource> sql = getSql(databaseVendor); // Configuration attestationServiceConf = ASConfig.getConfiguration(); DataSource ds = getDataSource(); log.debug("Connecting to {}", databaseVendor); Connection c = null; try { c = ds.getConnection(); // username and password should already be set in the datasource } catch(SQLException e) { log.error("Failed to connect to {} with schema: error =" + e.getMessage(), databaseVendor); log.error("Cannot connect to database"); System.exit(2); // throw e; // it's possible that the database connection is fine but the SCHEMA doesn't exist... so try connecting w/o a schema } // log.debug("Connected to schema: {}", c.getSchema()); List<ChangelogEntry> changelog = getChangelog(c); HashMap<Long,ChangelogEntry> presentChanges = new HashMap<Long,ChangelogEntry>(); // what is already in the database according to the changelog verbose("Existing database changelog has %d entries", changelog.size()); for(ChangelogEntry entry : changelog) { presentChanges.put(Long.valueOf(entry.id), entry); verbose("%s %s %s", entry.id, entry.applied_at, entry.description); } // Does it have any changes that we don't? In other words, is the database schema newer than what we know in this installer? if( options.getBoolean("check", false) ) { HashSet<Long> unknownChanges = new HashSet<Long>(presentChanges.keySet()); // list of what is in database unknownChanges.removeAll(sql.keySet()); // remove what we have in this installer if( unknownChanges.isEmpty() ) { System.out.println("Database is compatible"); // System.exit(0); // not yet -- after this block we'll print out if there are any changes to apply } else { // if( !unknownChanges.isEmpty() ) { // Database has new schema changes we dont' know about System.out.println("WARNING: Database schema is newer than this version of Mt Wilson"); ArrayList<Long> unknownChangesInOrder = new ArrayList<Long>(unknownChanges); Collections.sort(unknownChangesInOrder); for(Long unknownChangeId : unknownChangesInOrder) { ChangelogEntry entry = presentChanges.get(unknownChangeId); System.out.println(String.format("%s %s %s", entry.id, entry.applied_at, entry.description)); } System.exit(8); // database not compatible } } HashSet<Long> changesToApply = new HashSet<Long>(sql.keySet()); changesToApply.removeAll(presentChanges.keySet()); if( changesToApply.isEmpty() ) { System.out.println("No database updates available"); System.exit(0); // database is compatible; whether we are doing a dry run with --check or not, we exit here with success because there is nothing else to do } // At this point we know we have some updates to the databaseschema. ArrayList<Long> changesToApplyInOrder = new ArrayList<Long>(changesToApply); Collections.sort(changesToApplyInOrder); if(options.getBoolean("check", false)) { System.out.println("The following changes will be applied:"); for(Long changeId : changesToApplyInOrder) { /* ChangelogEntry entry = presentChanges.get(changeId); System.out.println(String.format("%s %s %s", entry.id, entry.applied_at, entry.description)); */ System.out.println(changeId); } System.exit(0); // database is compatible // return; } ResourceDatabasePopulator rdp = new ResourceDatabasePopulator(); // removing unneeded output as user can't choice what updates to apply //System.out.println("Available database updates:"); for(Long id : changesToApplyInOrder) { //System.out.println(String.format("%d %s", id, basename(sql.get(id).getURL()))); rdp.addScript(sql.get(id)); // new ClassPathResource("/com/intel/mtwilson/database/mysql/bootstrap.sql")); // must specify full path to resource } rdp.setContinueOnError(true); rdp.setIgnoreFailedDrops(true); rdp.setSeparator(";"); rdp.populate(c); c.close(); } // commenting out unused function (6/11 1.2) /* private String getDatabaseHostname(Connection c) throws SQLException { String hostname = null; Statement s = c.createStatement(); // ResultSet rs = s.executeQuery("SELECT @@version"); // output is 1 column (VARCHAR) with content like this: 5.1.63-0ubuntu0.11.10.1 // ResultSet rs = s.executeQuery("SELECT version()"); // output is same as for @@version ResultSet rs = s.executeQuery("SELECT @@hostname"); // output is 1 column (VARCHAR) with content like this: mtwilsondev (hostname of database server) if( rs.next() ) { int columns = rs.getMetaData().getColumnCount(); System.out.println("Got "+columns+" columns from datbase server"); System.out.println("First column type: "+rs.getMetaData().getColumnTypeName(1)); System.out.println("First column: "+rs.getString(1)); hostname = rs.getString(1); } rs.close(); s.close(); return hostname; } */ /** * Locates the SQL files for the specified vendor, and reads them to * create a mapping of changelog-date to SQL content. This mapping can * then be used to select which files to execute against an existing * database. * See also iBatis, which we are (very) roughly emulating. * @param databaseVendor * @return */ private Map<Long,Resource> getSql(String databaseVendor) throws SetupException { System.out.println("Scanning for "+databaseVendor+" SQL files"); HashMap<Long,Resource> sqlmap = new HashMap<Long,Resource>(); try { Resource[] list = listResources(databaseVendor); // each URL like: jar:file:/C:/Users/jbuhacof/workspace/mountwilson-0.5.4/desktop/setup-console/target/setup-console-0.5.4-SNAPSHOT-with-dependencies.jar!/com/intel/mtwilson/database/mysql/20121226000000_remove_created_by_patch_rc3.sql for(Resource resource : list) { URL url = resource.getURL(); // InputStream in = url.openStream(); // String sql = IOUtils.toString(in, "UTF-8"); // IOUtils.closeQuietly(in); Long timestamp = getTimestampFromSqlFilename(basename(url)); if( timestamp != null ) { sqlmap.put(timestamp, resource); } else { System.err.println("SQL filename is not in recognized format: "+url.toExternalForm()); } } } catch(IOException e) { throw new SetupException("Error while scanning for SQL files: "+e.getLocalizedMessage(), e); } //System.err.println("Number of SQL files: "+sqlmap.size()); return sqlmap; } private Resource[] listResources(String databaseVendor) throws IOException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); Resource[] resources = resolver.getResources("classpath:com/intel/mtwilson/database/"+databaseVendor+"/*.sql"); return resources; /* ArrayList<URL> list = new ArrayList<URL>(); for(Resource resource : resources) { list.add(resource.getURL()); } return list;*/ } Pattern pTimestampName = Pattern.compile("^([0-9]+).*"); /** * Given a URL, returns the final component filename * * Example URL: jar:file:/C:/Users/jbuhacof/workspace/mountwilson-0.5.4/desktop/setup-console/target/setup-console-0.5.4-SNAPSHOT-with-dependencies.jar!/com/intel/mtwilson/database/mysql/20121226000000_remove_created_by_patch_rc3.sql * Example output: 20121226000000_remove_created_by_patch_rc3.sql * @param url * @return */ private String basename(URL url) { String[] parts = StringUtils.split(url.toExternalForm(), "/"); return parts[parts.length-1]; } /** * @param filename without any path like: 20121226000000_remove_created_by_patch_rc3.sql * @return */ private Long getTimestampFromSqlFilename(String filename) { Matcher mTimestampName = pTimestampName.matcher(filename); if( mTimestampName.matches() ) { String timestamp = mTimestampName.group(1); // the timestamp like: 20121226000000 return Long.valueOf(timestamp); } return null; } // commenting out unused function (6/11 1.2) /* private void printSqlMap(Map<Long,String> sqlmap) { Set<Long> timestampSet = sqlmap.keySet(); ArrayList<Long> timestampList = new ArrayList<Long>(); timestampList.addAll(timestampSet); Collections.sort(timestampList); for(Long timestamp : timestampList) { System.out.println("File timestamp: "+timestamp); } } */ /** * * @return datasource object for mt wilson database, guaranteed non-null * @throws SetupException if the datasource cannot be obtained */ private DataSource getDataSource() throws SetupException { try { //Properties jpaProperties = MyPersistenceManager.getASDataJpaProperties(My.configuration()); Properties jpaProperties = MyPersistenceManager.getEnvDataJpaProperties(My.configuration()); log.debug("JDBC URL with schema: {}", jpaProperties.getProperty("javax.persistence.jdbc.url")); if( jpaProperties.getProperty("javax.persistence.jdbc.url") == null ) { log.error("Missing database connection settings"); System.exit(1); } DataSource ds = PersistenceManager.getPersistenceUnitInfo("ASDataPU", jpaProperties).getNonJtaDataSource(); if( ds == null ) { log.error("Cannot load persistence unit info"); System.exit(2); // throw new SetupException("Cannot load persistence unit info"); } log.debug("Loaded persistence unit: ASDataPU"); return ds; } catch(IOException e) { throw new SetupException("Cannot load persistence unit info", e); } } /* private DataSource getDataSourceNoSchema() throws SetupException { try { PropertyHidingConfiguration confNoSchema = new PropertyHidingConfiguration(ASConfig.getConfiguration()); confNoSchema.replaceProperty("mtwilson.db.schema",""); confNoSchema.replaceProperty("mountwilson.as.db.schema",""); confNoSchema.replaceProperty("mountwilson.ms.db.schema",""); Properties jpaProperties = MyPersistenceManager.getASDataJpaProperties(confNoSchema); log.debug("JDBC URL without schema: {}", jpaProperties.getProperty("javax.persistence.jdbc.url")); DataSource ds = PersistenceManager.getPersistenceUnitInfo("ASDataPU", jpaProperties).getNonJtaDataSource(); if( ds == null ) { throw new SetupException("Cannot load persistence unit info"); } log.debug("Loaded persistence unit: ASDataPU"); return ds; } catch(IOException e) { throw new SetupException("Cannot load persistence unit info", e); } } */ private List<String> getTableNames(Connection c) throws SQLException { ArrayList<String> list = new ArrayList<>(); try (Statement s = c.createStatement()) { String sql = ""; if (vendor.equals("mysql")) { sql = "SHOW TABLES"; } else if (vendor.equals("postgresql")) { sql = "SELECT table_name FROM information_schema.tables;"; } try (ResultSet rs = s.executeQuery(sql)) { while (rs.next()) { list.add(rs.getString(1)); } } } return list; } private List<ChangelogEntry> getChangelog(Connection c) throws SQLException { ArrayList<ChangelogEntry> list = new ArrayList<ChangelogEntry>(); log.debug("Listing tables..."); // first determine if we have the new changelog table `mw_changelog`, or the old one `changelog`, or none at all List<String> tableNames = getTableNames(c); boolean hasMwChangelog = false; boolean hasChangelog = false; if( tableNames.contains("mw_changelog") ) { hasMwChangelog = true; } if( tableNames.contains("changelog") ) { hasChangelog = true; } if( !hasChangelog && !hasMwChangelog) { return list; /* empty list indicates database is not initialized and all scripts need to be executed */ } String changelogTableName = null; // if we have both changelog tables, copy all records from old changelog to new changelog and then use that if( hasChangelog && hasMwChangelog) { try (PreparedStatement check = c.prepareStatement("SELECT APPLIED_AT FROM mw_changelog WHERE ID=?")) { try (PreparedStatement insert = c.prepareStatement("INSERT INTO mw_changelog SET ID=?, APPLIED_AT=?, DESCRIPTION=?")) { try (Statement select = c.createStatement()) { try (ResultSet rs = select.executeQuery("SELECT ID,APPLIED_AT,DESCRIPTION FROM changelog")) { while (rs.next()) { check.setLong(1, rs.getLong("ID")); try (ResultSet rsCheck = check.executeQuery()) { if (rsCheck.next()) { // the id is already in the new mw_changelog table } else { insert.setLong(1, rs.getLong("ID")); insert.setString(2, rs.getString("APPLIED_AT")); insert.setString(3, rs.getString("DESCRIPTION")); insert.executeUpdate(); } } } } } } } changelogTableName = "mw_changelog"; } else if( hasMwChangelog ) { changelogTableName = "mw_changelog"; } else if( hasChangelog ) { changelogTableName = "changelog"; } try (Statement s = c.createStatement()) { try (ResultSet rs = s.executeQuery(String.format("SELECT ID,APPLIED_AT,DESCRIPTION FROM %s", changelogTableName))) { while (rs.next()) { ChangelogEntry entry = new ChangelogEntry(); entry.id = rs.getString("ID"); entry.applied_at = rs.getString("APPLIED_AT"); entry.description = rs.getString("DESCRIPTION"); list.add(entry); } } } return list; } }