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

org.rhq.enterprise.server.installer.ServerInstallUtil Maven / Gradle / Ivy

There is a newer version: 4.13.0
Show newest version
/*
 * RHQ Management Platform
 * Copyright (C) 2005-2012 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.installer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.security.auth.login.AppConfigurationEntry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.helper.ProjectHelper2;

import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;
import org.jboss.sasl.util.UsernamePasswordHashUtil;

import org.rhq.common.jbossas.client.controller.Address;
import org.rhq.common.jbossas.client.controller.CoreJBossASClient;
import org.rhq.common.jbossas.client.controller.DatasourceJBossASClient;
import org.rhq.common.jbossas.client.controller.FailureException;
import org.rhq.common.jbossas.client.controller.InfinispanJBossASClient;
import org.rhq.common.jbossas.client.controller.JBossASClient;
import org.rhq.common.jbossas.client.controller.LoggingJBossASClient;
import org.rhq.common.jbossas.client.controller.MessagingJBossASClient;
import org.rhq.common.jbossas.client.controller.SecurityDomainJBossASClient;
import org.rhq.common.jbossas.client.controller.SocketBindingJBossASClient;
import org.rhq.common.jbossas.client.controller.TransactionsJBossASClient;
import org.rhq.common.jbossas.client.controller.WebJBossASClient;
import org.rhq.common.jbossas.client.controller.WebJBossASClient.ConnectorConfiguration;
import org.rhq.common.jbossas.client.controller.WebJBossASClient.SSLConfiguration;
import org.rhq.core.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.db.DbUtil;
import org.rhq.core.db.OracleDatabaseType;
import org.rhq.core.db.PostgresqlDatabaseType;
import org.rhq.core.db.setup.DBSetup;
import org.rhq.core.domain.cloud.StorageNode;
import org.rhq.core.domain.cloud.StorageNode.OperationMode;
import org.rhq.core.util.PropertiesFileUpdate;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.FileUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.communications.util.SecurityUtil;

/**
 * Provides utility methods necessary to complete the server installation.
 *
 * @author John Mazzitelli
 */
public class ServerInstallUtil {
    private static final Log LOG = LogFactory.getLog(ServerInstallUtil.class);

    public enum ExistingSchemaOption {
        OVERWRITE, KEEP, SKIP
    };

    public enum SupportedDatabaseType {
        POSTGRES, ORACLE
    };

    private static class SocketBindingInfo {
        public String name;
        public String sysprop;
        public int port;
        public boolean required = true;
        public String interfaceName = null; // not null if we know we want it to be changed from its default setting

        public SocketBindingInfo(String n, String s, int p) {
            this.name = n;
            this.sysprop = s;
            this.port = p;
        }

        public SocketBindingInfo(String n, String s, int p, String i) {
            this.name = n;
            this.sysprop = s;
            this.port = p;
            this.interfaceName = i;
        }

        public SocketBindingInfo(String name, String sysprop, int port, String interfaceName, boolean required) {
            this.name = name;
            this.sysprop = sysprop;
            this.port = port;
            this.interfaceName = interfaceName;
            this.required = required;
        }
    }

    private static final ArrayList defaultSocketBindings;
    static {
        // all ports are -1000 from out-of-box AS7 defaults
        // except for the jboss.management ones - those are -3000 from their out-of-box defaults
        defaultSocketBindings = new ArrayList();
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_AJP,
            "rhq.server.socket.binding.port.ajp", 7009));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_HTTP,
            "rhq.server.socket.binding.port.http", 7080));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_HTTPS,
            "rhq.server.socket.binding.port.https", 7443));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_JACORB,
            "rhq.server.socket.binding.port.jacorb", 2528));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_JACORB_SSL,
            "rhq.server.socket.binding.port.jacorb-ssl", 2529));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MESSAGING,
            "rhq.server.socket.binding.port.messaging", 4449, "management"));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MESSAGING_THRUPUT,
            "rhq.server.socket.binding.port.messaging-throughput", 4455, "management"));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_HTTP,
            "jboss.management.http.port", 6990));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_HTTPS,
            "jboss.management.https.port", 6443));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_NATIVE,
            "jboss.management.native.port", 6999));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_REMOTING,
            "rhq.server.socket.binding.port.remoting", 3447, "management"));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_TXN_RECOVERY_ENV,
            "rhq.server.socket.binding.port.txn-recovery-environment", 3712));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_TXN_STATUS_MGR,
            "rhq.server.socket.binding.port.txn-status-manager", 3713));
    }

    private static final String RHQ_DATASOURCE_NAME_NOTX = "NoTxRHQDS";
    private static final String RHQ_DATASOURCE_NAME_XA = "RHQDS";
    private static final String RHQ_DS_SECURITY_DOMAIN = "RHQDSSecurityDomain";
    private static final String RHQ_USER_SECURITY_DOMAIN = "RHQUserSecurityDomain";
    private static final String RHQ_REST_SECURITY_DOMAIN = "RHQRESTSecurityDomain";
    private static final String JDBC_LOGIN_MODULE_NAME = "org.rhq.enterprise.server.core.jaas.JDBCLoginModule";
    private static final String DELEGATIG_LOGIN_MODULE_NAME = "org.rhq.enterprise.server.core.jaas.DelegatingLoginModule";
    private static final String JDBC_DRIVER_POSTGRES = "postgres";
    private static final String JDBC_DRIVER_ORACLE = "oracle";
    private static final String JMS_ALERT_CONDITION_QUEUE = "AlertConditionQueue";
    private static final String JMS_DRIFT_CHANGESET_QUEUE = "DriftChangesetQueue";
    private static final String JMS_DRIFT_FILE_QUEUE = "DriftFileQueue";
    private static final String RHQ_CACHE_CONTAINER = "rhq";
    private static final String RHQ_CACHE = "rhqCache";
    private static final String RHQ_MGMT_USER = "rhqadmin";
    private static final String RHQ_MGMT_USER_PASSWORD = "rhq.server.management.password";

    /**
     * Configure the logging subsystem.
     * @param mcc JBossAS management client
     * @param serverProperties the server properties, which includes the default log level to use
     * @throws Exception
     */
    public static void configureLogging(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {
        LoggingJBossASClient client = new LoggingJBossASClient(mcc);

        // we want to create our own category
        String val = buildExpression(ServerProperties.PROP_LOG_LEVEL, serverProperties, true);
        client.setLoggerLevel("org.rhq", val);
        LOG.info("Logging category org.rhq set to [" + val + "]");

        client.setLoggerLevel("org.jboss.as.config", "INFO"); // BZ 1004730
    }

    /**
     * Configure the transaction manager.
     * @param mcc JBossAS management client
     * @throws Exception
     */
    public static void configureTransactionManager(ModelControllerClient mcc) throws Exception {
        TransactionsJBossASClient client = new TransactionsJBossASClient(mcc);

        // we want to bump up the transaction timeout
        client.setDefaultTransactionTimeout(600);
        LOG.info("Default transaction timeout set to 600 seconds.");
    }

    /**
     * Configure the deployment scanner.
     * @param mcc JBossAS management client
     * @throws Exception
     */
    public static void configureDeploymentScanner(ModelControllerClient mcc) throws Exception {
        CoreJBossASClient client = new CoreJBossASClient(mcc);

        // we do not want our RHQ Server to support hot deployments via the scanner
        client.setAppServerDefaultDeploymentScanEnabled(false);
        LOG.info("Deployment scanner turned off.");
    }

    /**
     * Prepares the mail service by configuring the SMTP settings.
     *
     * @param mcc JBossAS management client
     * @param serverProperties the server's properties
     * @throws Exception
     */
    public static void setupMailService(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {

        String fromAddressExpr = buildExpression(ServerProperties.PROP_EMAIL_FROM_ADDRESS, serverProperties, true);
        String smtpHostExpr = buildExpression(ServerProperties.PROP_EMAIL_SMTP_HOST, serverProperties, true);
        String smtpPortExpr = buildExpression(ServerProperties.PROP_EMAIL_SMTP_PORT, serverProperties, true);

        // Tweek the mail configuration that comes out of box. Setup a batch request to write the proper attributes.

        // First, the from address (TODO: there is also a "ssl", "username" and "password" attribute we could set for authz)
        Address addr = Address.root().add(JBossASClient.SUBSYSTEM, "mail", "mail-session", "java:jboss/mail/Default");
        ModelNode writeFromAddr = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writeFromAddr.get(JBossASClient.NAME).set("from");
        writeFromAddr.get(JBossASClient.VALUE).setExpression(fromAddressExpr);

        // now the SMTP host
        addr = Address.root().add("socket-binding-group", "standard-sockets",
            "remote-destination-outbound-socket-binding", "mail-smtp");
        ModelNode writeHost = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writeHost.get(JBossASClient.NAME).set("host");
        JBossASClient.setPossibleExpression(writeHost, JBossASClient.VALUE, smtpHostExpr);

        // now the SMTP port
        addr = Address.root().add("socket-binding-group", "standard-sockets",
            "remote-destination-outbound-socket-binding", "mail-smtp");
        ModelNode writePort = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writePort.get(JBossASClient.NAME).set("port");
        JBossASClient.setPossibleExpression(writePort, JBossASClient.VALUE, smtpPortExpr);

        ModelNode batch = JBossASClient.createBatchRequest(writeFromAddr, writeHost, writePort);
        JBossASClient client = new JBossASClient(mcc);
        ModelNode response = client.execute(batch);
        if (!JBossASClient.isSuccess(response)) {
            throw new FailureException(response, "Failed to setup mail service");
        }
        LOG.info("Mail service has been configured.");
        return;
    }

    /**
     * Give the server properties, this returns the type of database that will be connected to.
     *
     * @param serverProperties
     * @return the type of DB
     */
    public static SupportedDatabaseType getSupportedDatabaseType(HashMap serverProperties) {
        return getSupportedDatabaseType(serverProperties.get(ServerProperties.PROP_DATABASE_TYPE));
    }

    /**
     * Give the database type string, this returns the type of database that it refers to.
     *
     * @param dbType the database type string
     * @return the type of DB
     */
    public static SupportedDatabaseType getSupportedDatabaseType(String dbType) {
        if (dbType == null) {
            return null;
        }
        if (dbType.toLowerCase().indexOf("postgres") > -1) {
            return SupportedDatabaseType.POSTGRES;
        } else if (dbType.toLowerCase().indexOf("oracle") > -1) {
            return SupportedDatabaseType.ORACLE;
        }
        return null;
    }

    /**
     * Creates the security domain for the datasources. This is needed to support
     * obfuscation of the password in the configuration file.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createDatasourceSecurityDomain(ModelControllerClient mcc,
        HashMap serverProperties) throws Exception {

        final String dbUsername = buildExpression(ServerProperties.PROP_DATABASE_USERNAME, serverProperties, true);
        final String obfuscatedPassword = buildExpression(ServerProperties.PROP_DATABASE_PASSWORD, serverProperties,
            true);
        final SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        final String securityDomain = RHQ_DS_SECURITY_DOMAIN;
        if (!client.isSecurityDomain(securityDomain)) {
            client.createNewSecureIdentitySecurityDomain72(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Security domain [" + securityDomain + "] created");
        } else {
            LOG.info("Security domain [" + securityDomain + "] already exists, skipping the creation request");
            client.updateSecureIdentitySecurityDomainCredentials(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Credentials have been updated for security domain [" + securityDomain + "]");
        }
    }

    /**
     * Create the standard user security domain with the JDBCLogin module installed
     *
     * @param mcc ModelControllerClient to talk to the underlying AS
     * @throws Exception If anything goes wrong
     */
    public static void createUserSecurityDomain(ModelControllerClient mcc) throws Exception {

        Map options = new HashMap(2);
        options.put("hashAlgorithm", "MD5");
        options.put("hashEncoding", "base64");

        SecurityDomainJBossASClient.LoginModuleRequest loginModuleRequest = new SecurityDomainJBossASClient.LoginModuleRequest(
            JDBC_LOGIN_MODULE_NAME, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, options);

        SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        client.createNewSecurityDomain(RHQ_USER_SECURITY_DOMAIN, loginModuleRequest);

    }

    /**
     * Create a security domain for container managed security used with the rhq-rest.war
     * @param mcc ModelControllerClient to talk to the underlying AS.
     * @throws Exception If anything goes wrong
     */
    public static void createRestSecurityDomain(ModelControllerClient mcc) throws Exception {

        Map options = new HashMap(2);
        options.put("delegateTo", RHQ_USER_SECURITY_DOMAIN);
        options.put("roles", "rest-user");

        SecurityDomainJBossASClient.LoginModuleRequest loginModuleRequest = new SecurityDomainJBossASClient.LoginModuleRequest(
            DELEGATIG_LOGIN_MODULE_NAME, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, options);

        SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        client.createNewSecurityDomain(RHQ_REST_SECURITY_DOMAIN, loginModuleRequest);
    }

    /**
     * Creates the JMS Queues required for Drift and Alerting.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createNewJMSQueues(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {

        final MessagingJBossASClient client = new MessagingJBossASClient(mcc);
        final List entryNames = new ArrayList();

        // TODO (jshaughn): Prior to HornetQ we set recoveryRetries to 0: "don't redeliver messages on failure. It
        // just causes more failures. just go straight to the dead messages by setting recoveryRetries to 0.
        // This is equivalent to setting the dLQMaxResent property to 0 in the MessageDriven annotation in
        // the class definition."
        // HornetQ has different semantics, and may behave well with default settings. If not, we'll
        // likely need to add specific  elements for our queues, which set
        // max-delivery-attempts to 0.  The documented default is 10.

        String queueName = JMS_ALERT_CONDITION_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        queueName = JMS_DRIFT_CHANGESET_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        queueName = JMS_DRIFT_FILE_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        return;
    }

    /**
     * Creates the Infinispan caches for RHQ.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createNewCaches(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {

        final InfinispanJBossASClient client = new InfinispanJBossASClient(mcc);
        final String cacheContainerName = RHQ_CACHE_CONTAINER;
        final String localCacheName = RHQ_CACHE;
        if (!client.isCacheContainer(cacheContainerName)) {
            ModelNode request = client.createNewCacheContainerRequest(cacheContainerName, localCacheName);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Cache container [" + cacheContainerName + "]");
            } else {
                LOG.info("Cache container [" + cacheContainerName + "] created");
            }

        } else {
            LOG.info("Cache container [" + cacheContainerName + "] already exists, skipping the creation request");
        }

        if (!client.isLocalCache(cacheContainerName, localCacheName)) {
            ModelNode request = client.createNewLocalCacheRequest(cacheContainerName, localCacheName, null, null, null,
                null, null);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Local Cache [" + localCacheName + "]");
            } else {
                LOG.info("Local Cache [" + localCacheName + "] created");
            }

        } else {
            LOG.info("Local Cache [" + localCacheName + "] already exists, skipping the creation request");
        }
    }

    /**
     * Creates JDBC driver configurations so the datasources can properly connect to the backend databases.
     * This will attempt to create drivers for all supported databases, not just for the database type that
     * is currently configured.
     *
     * @param mcc
     * @param serverProperties
     * @throws Exception
     */
    public static void createNewJdbcDrivers(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {

        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        final ModelNode postgresDriverRequest = client.createNewJdbcDriverRequest(JDBC_DRIVER_POSTGRES,
            "org.rhq.postgres", "org.postgresql.xa.PGXADataSource");
        final ModelNode oracleDriverRequest = client.createNewJdbcDriverRequest(JDBC_DRIVER_ORACLE, "org.rhq.oracle",
            "oracle.jdbc.xa.client.OracleXADataSource");

        // if we are to use Oracle, we throw an exception if we can't create the Oracle datasource. We also try to
        // create the Postgres datasource but because it isn't needed, we don't throw exceptions if that fails, we
        // just log a warning.
        // The reverse is true if we are to use Postgres (that is, we ensure Postgres driver is created, but not Oracle).
        ModelNode results;
        final SupportedDatabaseType supportedDbType = getSupportedDatabaseType(serverProperties);
        switch (supportedDbType) {
        case POSTGRES: {
            if (client.isJDBCDriver(JDBC_DRIVER_POSTGRES)) {
                LOG.info("Postgres JDBC driver is already deployed");
            } else {
                results = client.execute(postgresDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    throw new FailureException(results, "Failed to create postgres database driver");
                } else {
                    LOG.info("Deployed Postgres JDBC driver");
                }
            }

            if (client.isJDBCDriver(JDBC_DRIVER_ORACLE)) {
                LOG.info("Oracle JDBC driver is already deployed");
            } else {
                results = client.execute(oracleDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    LOG.warn("Could not create Oracle JDBC Driver - you will not be able to switch to an Oracle DB later: "
                        + JBossASClient.getFailureDescription(results));
                } else {
                    LOG.info("Deployed Oracle JDBC driver for future use");
                }
            }
            break;
        }
        case ORACLE: {
            if (client.isJDBCDriver(JDBC_DRIVER_ORACLE)) {
                LOG.info("Oracle JDBC driver is already deployed");
            } else {
                results = client.execute(oracleDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    throw new FailureException(results, "Failed to create oracle database driver");
                } else {
                    LOG.info("Deployed Oracle JDBC driver");
                }
            }
            if (client.isJDBCDriver(JDBC_DRIVER_POSTGRES)) {
                LOG.info("Postgres JDBC driver is already deployed");
            } else {
                results = client.execute(postgresDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    LOG.warn("Could not create Postgres JDBC Driver - you will not be able to switch to a Postgres DB later: "
                        + JBossASClient.getFailureDescription(results));
                } else {
                    LOG.info("Deployed Postgres JDBC driver for future use");
                }
            }
            break;
        }
        default:
            throw new RuntimeException("bad db type"); // this should never happen; should have never gotten to this point with a bad type
        }
    }

    /**
     * Creates the datasources needed by the RHQ Server.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties properties to help determine the properties of the datasources to be created
     * @throws Exception
     */
    public static void createNewDatasources(ModelControllerClient mcc, HashMap serverProperties)
        throws Exception {

        final SupportedDatabaseType supportedDbType = getSupportedDatabaseType(serverProperties);
        switch (supportedDbType) {
        case POSTGRES: {
            createNewDatasources_Postgres(mcc);
            break;
        }
        case ORACLE: {
            createNewDatasources_Oracle(mcc);
            break;
        }
        default:
            throw new RuntimeException("bad db type"); // this should never happen; should have never gotten to this point with a bad type
        }
        LOG.info("Created datasources");

        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);
        client.enableDatasource(RHQ_DATASOURCE_NAME_NOTX);
        client.enableXADatasource(RHQ_DATASOURCE_NAME_XA);
        LOG.info("Enabled datasources");

    }

    private static void createNewDatasources_Postgres(ModelControllerClient mcc) throws Exception {
        final HashMap props = new HashMap(4);
        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        ModelNode noTxDsRequest = null;
        ModelNode xaDsRequest = null;

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_NOTX)) {
            props.put("char.encoding", "UTF-8");

            noTxDsRequest = client.createNewDatasourceRequest(RHQ_DATASOURCE_NAME_NOTX, 30000,
                "${rhq.server.database.connection-url:jdbc:postgres://127.0.0.1:5432/rhq}", JDBC_DRIVER_POSTGRES,
                "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter", 15, false, 2, 5, 75,
                RHQ_DS_SECURITY_DOMAIN, "-unused-stale-conn-checker-", "TRANSACTION_READ_COMMITTED",
                "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker", props);
            noTxDsRequest.get("steps").get(0).remove("stale-connection-checker-class-name"); // we don't have one of these for postgres
        } else {
            LOG.info("Postgres datasource [" + RHQ_DATASOURCE_NAME_NOTX + "] already exists");
        }

        if (!client.isXADatasource(RHQ_DATASOURCE_NAME_XA)) {
            props.clear();
            props.put("ServerName", "${rhq.server.database.server-name:127.0.0.1}");
            props.put("PortNumber", "${rhq.server.database.port:5432}");
            props.put("DatabaseName", "${rhq.server.database.db-name:rhq}");

            xaDsRequest = client.createNewXADatasourceRequest(RHQ_DATASOURCE_NAME_XA, 30000, JDBC_DRIVER_POSTGRES,
                "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter", 15, 5, 50, (Boolean) null,
                (Boolean) null, 75, (String) null, RHQ_DS_SECURITY_DOMAIN, (String) null, "TRANSACTION_READ_COMMITTED",
                "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker", props);

        } else {
            LOG.info("Postgres XA datasource [" + RHQ_DATASOURCE_NAME_XA + "] already exists");
        }

        if (noTxDsRequest != null || xaDsRequest != null) {
            ModelNode batch = DatasourceJBossASClient.createBatchRequest(noTxDsRequest, xaDsRequest);
            ModelNode results = client.execute(batch);
            if (!DatasourceJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Postgres datasources");
            }
        }
    }

    private static void createNewDatasources_Oracle(ModelControllerClient mcc) throws Exception {
        final HashMap props = new HashMap(2);
        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        ModelNode noTxDsRequest = null;
        ModelNode xaDsRequest = null;

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_NOTX)) {
            props.put("char.encoding", "UTF-8");

            noTxDsRequest = client.createNewDatasourceRequest(RHQ_DATASOURCE_NAME_NOTX, 30000,
                "${rhq.server.database.connection-url:jdbc:oracle:thin:@127.0.0.1:1521:rhq}", JDBC_DRIVER_ORACLE,
                "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleExceptionSorter", 15, false, 2, 5, 75,
                RHQ_DS_SECURITY_DOMAIN, "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleStaleConnectionChecker",
                "TRANSACTION_READ_COMMITTED",
                "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleValidConnectionChecker", props);
        } else {
            LOG.info("Oracle datasource [" + RHQ_DATASOURCE_NAME_NOTX + "] already exists");
        }

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_XA)) {
            props.clear();
            props.put("URL", "${rhq.server.database.connection-url:jdbc:oracle:thin:@127.0.0.1:1521:rhq}");

            xaDsRequest = client.createNewXADatasourceRequest(RHQ_DATASOURCE_NAME_XA, 30000, JDBC_DRIVER_ORACLE,
                "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleExceptionSorter", 15, 5, 50, (Boolean) null,
                Boolean.TRUE, 75, (String) null, RHQ_DS_SECURITY_DOMAIN,
                "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleStaleConnectionChecker",
                "TRANSACTION_READ_COMMITTED",
                "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleValidConnectionChecker", props);
        } else {
            LOG.info("Oracle XA datasource [" + RHQ_DATASOURCE_NAME_XA + "] already exists");
        }

        if (noTxDsRequest != null || xaDsRequest != null) {
            ModelNode batch = DatasourceJBossASClient.createBatchRequest(noTxDsRequest, xaDsRequest);
            ModelNode results = client.execute(batch);
            if (!DatasourceJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Oracle datasources");
            }
        }
    }

    /**
     * Determines if we are in auto-install mode. This means the properties file is
     * fully configured and the installation can begin without asking the user
     * for more input.
     *
     * @param serverProperties the full set of server properties
     *
     * @return true if we are in auto-install mode; false if the user must give us more
     *         information before we can complete the installation.
     */
    public static boolean isAutoinstallEnabled(HashMap serverProperties) {
        String enableProp = serverProperties.get(ServerProperties.PROP_AUTOINSTALL_ENABLE);
        if (enableProp != null) {
            return Boolean.parseBoolean(enableProp);
        }
        return false;
    }

    /**
     * Returns true if the database already has the database schema created for it. It will not be known
     * what version of schema or if its the latest, all this method tells you is that some RHQ database schema exists.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return true if the database can be connected to
     *
     * @throws Exception if failed to communicate with the database
     */
    public static boolean isDatabaseSchemaExist(String connectionUrl, String username, String password)
        throws Exception {

        Connection conn = getDatabaseConnection(connectionUrl, username, password);
        DatabaseType db = DatabaseTypeFactory.getDatabaseType(conn);

        try {
            return db.checkTableExists(conn, "RHQ_PRINCIPAL");
        } catch (IllegalStateException e) {
            return false;
        } finally {
            db.closeConnection(conn);
        }
    }

    /**
     * Get the list of existing servers from an existing schema.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return List of server names registered in the database. Empty list if the table does not exist or there are no entries in the table.
     *
     * @throws Exception if failed to communicate with the database
     */
    public static ArrayList getServerNames(String connectionUrl, String username, String password)
        throws Exception {
        DatabaseType db = null;
        Connection conn = null;
        Statement stm = null;
        ResultSet rs = null;
        ArrayList result = new ArrayList();

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            if (db.checkTableExists(conn, "rhq_server")) {

                stm = conn.createStatement();
                rs = stm.executeQuery("SELECT name FROM rhq_server ORDER BY name asc");

                while (rs.next()) {
                    result.add(rs.getString(1));
                }
            }
        } catch (IllegalStateException e) {
            // table does not exist
        } catch (SQLException e) {
            LOG.info("Unable to fetch existing server info: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeJDBCObjects(conn, stm, rs);
            }
        }

        return result;
    }

    /**
     * Returns information on the server as found in the database (port numbers, affinity group, etc).
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @param serverName the server whose details are to be returned
     * @return the information on the named server
     */
    public static ServerDetails getServerDetails(String connectionUrl, String username, String password,
        String serverName) {

        DatabaseType db = null;
        Connection conn = null;
        ServerDetails result = null;

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            result = getServerDetails(db, conn, serverName);

        } catch (Exception e) {
            LOG.info("Unable to get server detail: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeConnection(conn);
            }
        }

        return result;
    }

    private static ServerDetails getServerDetails(DatabaseType db, Connection conn, String serverName) {
        PreparedStatement stm = null;
        ResultSet rs = null;
        ServerDetails result = null;

        if (null == serverName) {
            return result;
        }

        try {
            stm = conn.prepareStatement("" //
                + "SELECT s.address, s.port, s.secure_port " //
                + "  FROM rhq_server s " //
                + " WHERE s.name = ?");
            stm.setString(1, serverName.trim());

            rs = stm.executeQuery();

            if (rs.next()) {
                result = new ServerDetails(serverName, rs.getString(1), rs.getInt(2), rs.getInt(3));
            }
        } catch (SQLException e) {
            LOG.info("Unable to get server details for server [" + serverName + "]: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeResultSet(rs);
                db.closeStatement(stm);
            }
        }

        return result;
    }

    /**
     * Tests to make sure the server can be connected to with the given settings.
     * If the test is successful, null. If the test fails, the returned string
     * will be the error message to indicate the problem.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return error message if test failed; null if test succeeded
     */
    public static String testConnection(String connectionUrl, String username, String password) {

        // its possible the JDBC URL was changed, clear the factory cache in case the DB version is different now
        DatabaseTypeFactory.clearDatabaseTypeCache();

        try {
            ensureDatabaseIsSupported(connectionUrl, username, password);
            return null;
        } catch (Exception e) {
            LOG.warn("Installer failed to test connection", e);
            return ThrowableUtil.getAllMessages(e);
        }
    }

    /**
     * Call this when you need to confirm that the database is supported.
     *
     * @param connectionUrl
     * @param username
     * @param password
     *
     * @throws Exception if the database is not supported
     */
    public static void ensureDatabaseIsSupported(String connectionUrl, String username, String password)
        throws Exception {
        Connection conn = null;
        DatabaseType db = null;

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            String version = db.getVersion();

            if (DatabaseTypeFactory.isPostgres(db)) {
                if (version.startsWith("7") || version.equals("8") || version.startsWith("8.0")
                    || version.startsWith("8.1")) {
                    throw new Exception("Unsupported PostgreSQL [" + db + "]");
                }
            } else if (DatabaseTypeFactory.isOracle(db)) {
                if (version.startsWith("8") || version.startsWith("9")) {
                    throw new Exception("Unsupported Oracle [" + db + "]");
                }
            } else {
                throw new Exception("Unsupported DB [" + db + "]");
            }

            LOG.info("Database is supported: " + db);
        } finally {
            if (db != null) {
                db.closeConnection(conn);
            }
        }

        return;
    }

    /**
     * Returns a database connection with the given set of properties providing the settings that allow for a successful
     * database connection. If props is null, it will use the server properties from
     * {@link #getServerProperties}.
     *
     * @param connectionUrl
     * @param userName
     * @param password
     * @return the database connection
     *
     * @throws SQLException if cannot successfully connect to the database
     */
    public static Connection getDatabaseConnection(String connectionUrl, String userName, String password)
        throws SQLException {
        return DbUtil.getConnection(connectionUrl, userName, password);
    }

    /**
     * Persists the storage nodes to the database only if no storage node entities already exist. This method is used
     * to persist storage nodes created from the rhq.storage.nodes server configuration property. The only time those
     * seed nodes should be created is during an initial server installation. After the initial installation storage
     * nodes should be created using rhqctl install. This ensures that any necessary cluster maintenance
     * tasks will be performed.
     *
     * @param serverProperties the server properties
     * @param password clear text password to connect to the database
     * @param storageNodes the {@link StorageNode storage nodes} to persist
     * @throws Exception
     */
    public static void persistStorageNodesIfNecessary(HashMap serverProperties, String password,
        Set storageNodes) throws Exception {
        DatabaseType db = null;
        Connection connection = null;
        Statement queryStatement = null;
        ResultSet resultSet = null;
        PreparedStatement insertStorageNode = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            queryStatement = connection.createStatement();
            resultSet = queryStatement.executeQuery("SELECT count(id) FROM rhq_storage_node");
            resultSet.next();

            if (resultSet.getInt(1) == 0) {
                connection.setAutoCommit(false);

                try {
                    LOG.info("Persisting to database new storage nodes for values specified in server configuration property [rhq.storage.nodes]");

                    insertStorageNode = connection
                        .prepareStatement("INSERT INTO rhq_storage_node (id, address, cql_port, operation_mode, ctime, mtime, maintenance_pending) "
                            + "VALUES (?, ?, ?, ?, ?, ?, ?)");

                    int id = 1001;
                    for (StorageNode storageNode : storageNodes) {
                        insertStorageNode.setInt(1, id);
                        insertStorageNode.setString(2, storageNode.getAddress());
                        insertStorageNode.setInt(3, storageNode.getCqlPort());
                        insertStorageNode.setString(4, StorageNode.OperationMode.INSTALLED.toString());
                        insertStorageNode.setLong(5, System.currentTimeMillis());
                        insertStorageNode.setLong(6, System.currentTimeMillis());
                        insertStorageNode.setBoolean(7, false);

                        insertStorageNode.executeUpdate();
                        id += 1;
                    }

                    connection.commit();
                } catch (SQLException e) {
                    LOG.error("Failed to persist to database the storage nodes specified by server configuration "
                        + "property [rhq.storage.nodes]. Transaction will be rolled back.", e);
                    connection.rollback();
                    throw e;
                }
            } else {
                LOG.info("Storage nodes already exist in database. Server configuration property [rhq.storage.nodes] will be ignored.");
            }

        } finally {
            if (db != null) {
                db.closeResultSet(resultSet);
                db.closeStatement(queryStatement);
                db.closeStatement(insertStorageNode);
                db.closeConnection(connection);
            }
        }
    }

    public static Map fetchStorageClusterSettings(HashMap serverProperties,
        String password) throws Exception {

        Map result = new HashMap(5);
        DatabaseType db = null;
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            try {
                statement = connection.prepareStatement("" //
                    + "SELECT property_key, property_value FROM rhq_system_config " //
                    + " WHERE property_key LIKE 'STORAGE%' " //
                    + "   AND NOT property_value IS NULL ");
                resultSet = statement.executeQuery();

                while (resultSet.next()) {
                    String key = resultSet.getString(1);
                    String value = resultSet.getString(2);

                    if (key.equals("STORAGE_USERNAME")) {
                        result.put(ServerProperties.PROP_STORAGE_USERNAME, value);
                    } else if (key.equals("STORAGE_PASSWORD")) {
                        result.put(ServerProperties.PROP_STORAGE_PASSWORD, value);
                    } else if (key.equals("STORAGE_GOSSIP_PORT")) {
                        result.put(ServerProperties.PROP_STORAGE_GOSSIP_PORT, value);
                    } else if (key.equals("STORAGE_CQL_PORT")) {
                        result.put(ServerProperties.PROP_STORAGE_CQL_PORT, value);
                    }
                }
            } finally {
                db.closeResultSet(resultSet);
                db.closeStatement(statement);
            }

            try {
                statement = connection.prepareStatement("" //
                    + "SELECT address FROM rhq_storage_node " //
                    + " WHERE operation_mode in " + "('" + OperationMode.NORMAL.name()
                    + "', '"
                    + OperationMode.INSTALLED.name() + "') ");
                resultSet = statement.executeQuery();

                StringBuffer addressList = new StringBuffer();
                while (resultSet.next()) {
                    String address = resultSet.getString(1);

                    if(address != null && !address.trim().isEmpty()){
                        if (addressList.length() != 0) {
                            addressList.append(',');
                        }
                        addressList.append(address);
                    }
                }

                if (addressList.length() != 0) {
                    result.put(ServerProperties.PROP_STORAGE_NODES, addressList.toString());
                }
            } finally {
                db.closeResultSet(resultSet);
                db.closeStatement(statement);
            }
        } catch (SQLException e) {
            LOG.error("Failed to fetch storage cluster settings.", e);
            throw e;
        } finally {
            if (db != null) {
                db.closeConnection(connection);
            }
        }

        return result;
    }

    public static void persistStorageClusterSettingsIfNecessary(HashMap serverProperties,
        String password) throws Exception {
        DatabaseType db = null;
        Connection connection = null;
        PreparedStatement updateClusterSetting = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            connection = getDatabaseConnection(dbUrl, userName, password);
            connection.setAutoCommit(false);

            updateClusterSetting = connection.prepareStatement("" //
                + "UPDATE rhq_system_config " //
                + "   SET property_value = ?, default_property_value = ? " //
                + " WHERE property_key = ? " //
                + "   AND ( property_value IS NULL OR property_value = '' OR property_value = 'UNSET' ) ");

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_USERNAME));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_USERNAME));
            updateClusterSetting.setString(3, "STORAGE_USERNAME");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_PASSWORD));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_PASSWORD));
            updateClusterSetting.setString(3, "STORAGE_PASSWORD");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_CQL_PORT));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_CQL_PORT));
            updateClusterSetting.setString(3, "STORAGE_CQL_PORT");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_GOSSIP_PORT));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_GOSSIP_PORT));
            updateClusterSetting.setString(3, "STORAGE_GOSSIP_PORT");
            updateClusterSetting.executeUpdate();

            connection.commit();
        } catch (SQLException e) {
            LOG.error("Failed to initialize storage cluster settings. Transaction will be rolled back.", e);
            connection.rollback();
            throw e;
        } finally {
            if (db != null) {
                db.closeStatement(updateClusterSetting);
                db.closeConnection(connection);
            }
        }
    }

    /**
     * Stores the server details (such as the public endpoint) in the database. If the server definition already
     * exists, it will be updated; otherwise, a new server will be added to the HA cloud.
     *
     * @param serverProperties the server properties
     * @param password clear text password to connect to the database
     * @param serverDetails the details of the server to put into the database
     * @throws Exception
     */
    public static void storeServerDetails(HashMap serverProperties, String password,
        ServerDetails serverDetails) throws Exception {

        DatabaseType db = null;
        Connection conn = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            conn = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            updateOrInsertServer(db, conn, serverDetails);

        } catch (SQLException e) {
            // TODO: should we throw an exception here? This would abort the rest of the installation
            LOG.info("Unable to store server entry in the database: " + ThrowableUtil.getAllMessages(e));
        } finally {
            if (null != db) {
                db.closeConnection(conn);
            }
        }
    }

    private static void updateOrInsertServer(DatabaseType db, Connection conn, ServerDetails serverDetails) {
        PreparedStatement stm = null;
        ResultSet rs = null;

        if (null == serverDetails || isEmpty(serverDetails.getName())) {
            return;
        }

        try {
            stm = conn.prepareStatement("UPDATE rhq_server SET address=?, port=?, secure_port=? WHERE name=?");
            stm.setString(1, serverDetails.getEndpointAddress());
            stm.setInt(2, serverDetails.getEndpointPort());
            stm.setInt(3, serverDetails.getEndpointSecurePort());
            stm.setString(4, serverDetails.getName());
            if (0 == stm.executeUpdate()) {
                stm.close();

                // set all new servers to operation_mode=INSTALLED
                int i = 1;
                if (db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType) {
                    stm = conn.prepareStatement("INSERT INTO rhq_server " //
                        + " ( id, name, address, port, secure_port, ctime, mtime, operation_mode, compute_power ) " //
                        + "VALUES ( ?, ?, ?, ?, ?, ?, ?, 'INSTALLED', 1 )");
                    stm.setInt(i++, db.getNextSequenceValue(conn, "rhq_server", "id"));
                } else {
                    throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
                }

                stm.setString(i++, serverDetails.getName());
                stm.setString(i++, serverDetails.getEndpointAddress());
                stm.setInt(i++, serverDetails.getEndpointPort());
                stm.setInt(i++, serverDetails.getEndpointSecurePort());
                long now = System.currentTimeMillis();
                stm.setLong(i++, now);
                stm.setLong(i++, now);
                stm.executeUpdate();
            }

        } catch (SQLException e) {
            LOG.info("Unable to put the server details in the database: " + ThrowableUtil.getAllMessages(e));
        } finally {
            if (null != db) {
                db.closeResultSet(rs);
                db.closeStatement(stm);
            }
        }
    }

    /**
     * This will create the database schema in the database. props define the connection to the database -
     *
     * 

Note that if the {@link #isDatabaseSchemaExist schema already exists}, it will be purged of all * data/tables and recreated.

* * @param props the full set of server properties * @param serverDetails additional information about the server being installed * @param password the database password in clear text * @param logDir a directory where the db schema upgrade logs can be written * * @throws Exception if failed to create the new schema for some reason */ public static void createNewDatabaseSchema(HashMap props, ServerDetails serverDetails, String password, String logDir) throws Exception { String dbUrl = props.get(ServerProperties.PROP_DATABASE_CONNECTION_URL); String userName = props.get(ServerProperties.PROP_DATABASE_USERNAME); try { // extract the dbsetup files which are located in the dbutils jar String dbsetupSchemaXmlFile = extractDatabaseXmlFile("db-schema-combined.xml", props, serverDetails, logDir); String dbsetupDataXmlFile = extractDatabaseXmlFile("db-data-combined.xml", props, serverDetails, logDir); // first uninstall any old existing schema, then create the tables then insert the data DBSetup dbsetup = new DBSetup(dbUrl, userName, password); dbsetup.uninstall(dbsetupSchemaXmlFile); dbsetup.setup(dbsetupSchemaXmlFile); dbsetup.setup(dbsetupDataXmlFile, null, true, false); } catch (Exception e) { LOG.fatal("Cannot install the database schema - the server will not run properly.", e); throw e; } return; } /** * This will update an existing database schema so it can be upgraded to the latest schema version. * *

Note that if the {@link #isDatabaseSchemaExist schema does not already exist}, errors will * occur.

* * @param props the full set of server properties * @param serverDetails additional information about the server being installed * @param password the database password in clear text * @param logDir a directory where the db schema upgrade logs can be written * * @throws Exception if the upgrade failed for some reason */ public static void upgradeExistingDatabaseSchema(HashMap props, ServerDetails serverDetails, String password, String logDir) throws Exception { String dbUrl = props.get(ServerProperties.PROP_DATABASE_CONNECTION_URL); String userName = props.get(ServerProperties.PROP_DATABASE_USERNAME); File logfile = new File(logDir, "rhq-installer-dbupgrade.log"); logfile.delete(); // do not keep logs from previous dbupgrade runs try { // extract the dbupgrade ANT script which is located in the dbutils jar String dbupgradeXmlFile = extractDatabaseXmlFile("db-upgrade.xml", props, serverDetails, logDir); Properties antProps = new Properties(); antProps.setProperty("jdbc.url", dbUrl); antProps.setProperty("jdbc.user", userName); antProps.setProperty("jdbc.password", password); antProps.setProperty("target.schema.version", "LATEST"); startAnt(new File(dbupgradeXmlFile), "db-ant-tasks.properties", antProps, logfile); } catch (Exception e) { LOG.fatal("Cannot upgrade the database schema - the server will not run properly.", e); throw e; } return; } /** * Given a server property value string, returns true if it is not specified. * * @param s the property string value * * @return true if it is null or empty */ public static boolean isEmpty(String s) { return s == null || s.trim().length() == 0; } /** * Takes the named XML file from the classloader and writes the file to the log directory. This is meant to extract * the schema/data xml files from the dbutils jar file. It can also be used to extract the db upgrade XML file. * * @param xmlFileName the name of the XML file, as found in the classloader * @param props properties whose values are used to replace the replacement strings found in the XML file * @param serverDetails additional information about the server being installed * @param logDir a directory where the db schema upgrade logs can be written * * @return the absolute path to the extracted file * * @throws IOException if failed to extract the file to the log directory */ private static String extractDatabaseXmlFile(String xmlFileName, HashMap props, ServerDetails serverDetails, String logDir) throws IOException { // first slurp the file contents in memory InputStream resourceInStream = ServerInstallUtil.class.getClassLoader().getResourceAsStream(xmlFileName); ByteArrayOutputStream contentOutStream = new ByteArrayOutputStream(); StreamUtil.copy(resourceInStream, contentOutStream); // now replace their replacement strings with values from the properties String emailFromAddress = props.get(ServerProperties.PROP_EMAIL_FROM_ADDRESS); if (isEmpty(emailFromAddress)) { emailFromAddress = "rhqadmin@localhost"; } String httpPort = props.get(ServerProperties.PROP_WEB_HTTP_PORT); if (isEmpty(httpPort)) { httpPort = String.valueOf(ServerDetails.DEFAULT_ENDPOINT_PORT); } String publicEndpoint = serverDetails.getEndpointAddress(); if (isEmpty(publicEndpoint)) { try { publicEndpoint = props.get(ServerProperties.PROP_JBOSS_BIND_ADDRESS); if (isEmpty(publicEndpoint) || ("0.0.0.0".equals(publicEndpoint))) { publicEndpoint = InetAddress.getLocalHost().getHostAddress(); } } catch (Exception e) { publicEndpoint = "127.0.0.1"; } } String content = contentOutStream.toString(); content = content.replaceAll("@@@LARGE_TABLESPACE_FOR_DATA@@@", "DEFAULT"); content = content.replaceAll("@@@LARGE_TABLESPACE_FOR_INDEX@@@", "DEFAULT"); content = content.replaceAll("@@@ADMINUSERNAME@@@", "rhqadmin"); content = content.replaceAll("@@@ADMINPASSWORD@@@", "x1XwrxKuPvYUILiOnOZTLg=="); // rhqadmin content = content.replaceAll("@@@ADMINEMAIL@@@", emailFromAddress); content = content.replaceAll("@@@BASEURL@@@", "http://" + publicEndpoint + ":" + httpPort + "/"); content = content.replaceAll("@@@JAASPROVIDER@@@", "JDBC"); content = content.replaceAll("@@@LDAPURL@@@", "ldap://localhost/"); content = content.replaceAll("@@@LDAPPROTOCOL@@@", ""); content = content.replaceAll("@@@LDAPLOGINPROP@@@", "cn"); content = content.replaceAll("@@@LDAPBASEDN@@@", "o=JBoss,c=US"); content = content.replaceAll("@@@LDAPSEARCHFILTER@@@", ""); content = content.replaceAll("@@@LDAPBINDDN@@@", ""); content = content.replaceAll("@@@LDAPBINDPW@@@", ""); content = content.replaceAll("@@@MULTICAST_ADDR@@@", ""); content = content.replaceAll("@@@MULTICAST_PORT@@@", ""); // we now have the finished XML content - write out the file to the log directory File xmlFile = new File(logDir, xmlFileName); FileOutputStream xmlFileOutStream = new FileOutputStream(xmlFile); ByteArrayInputStream contentInStream = new ByteArrayInputStream(content.getBytes()); StreamUtil.copy(contentInStream, xmlFileOutStream); return xmlFile.getAbsolutePath(); } /** * Launches ANT and runs the default target in the given build file. * * @param buildFile the build file that ANT will run * @param customTaskDefs the properties file found in classloader that contains all the taskdef definitions * @param properties set of properties to set for the ANT task to access * @param logFile where ANT messages will be logged (in addition to the app server's log file) * * @throws RuntimeException */ private static void startAnt(File buildFile, String customTaskDefs, Properties properties, File logFile) { PrintWriter logFileOutput = null; try { logFileOutput = new PrintWriter(new FileOutputStream(logFile)); ClassLoader classLoader = ServerInstallUtil.class.getClassLoader(); Properties taskDefs = new Properties(); InputStream taskDefsStream = classLoader.getResourceAsStream(customTaskDefs); try { taskDefs.load(taskDefsStream); } finally { taskDefsStream.close(); } Project project = new Project(); project.setCoreLoader(classLoader); project.init(); for (Map.Entry property : properties.entrySet()) { project.setProperty(property.getKey().toString(), property.getValue().toString()); } // notice we add our listener after we set the properties - we do not want the password to be in the log file // our dbupgrade script will echo the property settings, so we can still get the other values project.addBuildListener(new LoggerAntBuildListener(logFileOutput)); for (Map.Entry taskDef : taskDefs.entrySet()) { project.addTaskDefinition(taskDef.getKey().toString(), Class.forName(taskDef.getValue().toString(), true, classLoader)); } new ProjectHelper2().parse(project, buildFile); project.executeTarget(project.getDefaultTarget()); } catch (Exception e) { throw new RuntimeException("Cannot run ANT on script [" + buildFile + "]. Cause: " + e, e); } finally { if (logFileOutput != null) { logFileOutput.close(); } } } /** * Ensures our web connectors are configured properly. * * @param mcc the AS client * @param configDirStr location of a configuration directory where the keystore is to be stored * @param serverProperties the full set of server properties * @throws Exception */ public static void setupWebConnectors(ModelControllerClient mcc, String configDirStr, HashMap serverProperties) throws Exception { // out of box, we always get a non-secure connector (called "http")... final String connectorName = "http"; // ...but we want a secure SSL connector, too. // This is the name of the secure connector we want to create. final String sslConnectorName = "https"; WebJBossASClient client = new WebJBossASClient(mcc); // because some of the connector attributes do not (yet) support expressions, let's remove any existing // connector we may have created before and create it again with our current attribute values. client.removeConnector(sslConnectorName); LOG.info("Creating https connector..."); ConnectorConfiguration connector = buildSecureConnectorConfiguration(configDirStr, serverProperties); // verify that we have a truststore file - if user is relying on our self-signed certs, we'll have to create one for them String truststoreFileString = connector.getSslConfiguration().getCaCertificateFile(); truststoreFileString = resolveExpression(mcc, truststoreFileString); if (truststoreFileString == null) { LOG.warn("Missing a valid truststore location - you must specify a valid truststore location!"); } else { File truststoreFile = new File(truststoreFileString); if (!truststoreFile.exists()) { // user didn't provide a truststore file, copy the keystore and use it as the truststore; tell the user about this String keystoreFileString = connector.getSslConfiguration().getCertificateKeyFile(); keystoreFileString = resolveExpression(mcc, keystoreFileString); File keystoreFile = new File(keystoreFileString); if (!keystoreFile.isFile()) { LOG.warn("Missing both keystore [" + keystoreFile + "] and truststore [" + truststoreFile + "]"); } else { LOG.warn("Missing the truststore [" + truststoreFile + "] - will copy the keystore [" + keystoreFile + "] and make the copy the truststore."); try { FileUtil.copyFile(keystoreFile, truststoreFile); } catch (Exception e) { LOG.error("Failed to copy keystore to make truststore - a truststore still does not exist", e); } } } } client.addConnector("https", connector); LOG.info("https connector created."); if (client.isConnector(connectorName)) { client.changeConnector(connectorName, "max-connections", buildExpression("rhq.server.startup.web.max-connections", serverProperties, true)); client.changeConnector(connectorName, "redirect-port", buildExpression("rhq.server.socket.binding.port.https", serverProperties, true)); } else { LOG.warn("There doesn't appear to be a http connector configured already - this is strange."); } } private static String resolveExpression(ModelControllerClient mcc, String expression) { if (expression == null) { return null; } CoreJBossASClient client = new CoreJBossASClient(mcc); String resolvedExpression; try { resolvedExpression = client.resolveExpression(expression); // https://issues.jboss.org/browse/WFLY-1177 - app server doesn't do recursive resolving, we have to do it here while (resolvedExpression != null && resolvedExpression.contains("${") && !resolvedExpression.equals(expression)) { expression = resolvedExpression; resolvedExpression = client.resolveExpression(expression); } } catch (Exception e) { LOG.warn("Cannot resolve expression [" + expression + "]; will use it as-is but errors may occur later."); resolvedExpression = expression; } return resolvedExpression; } private static ConnectorConfiguration buildSecureConnectorConfiguration(String configDirStr, HashMap serverProperties) { SSLConfiguration ssl = new SSLConfiguration(); // Because of https://issues.jboss.org/browse/WFLY-1177 we cannot build expressions for key/truststore files. // Otherwise, we end up with recursive expressions (${${x}:a}) which is what's broken. For now, just use ${x} which is allowed. // truststore ssl.setCaCertificateFile(buildExpression("rhq.server.tomcat.security.truststore.file", serverProperties, false)); ssl.setCaCertificationPassword(buildExpression("rhq.server.tomcat.security.truststore.password", serverProperties, true)); ssl.setTruststoreType(buildExpression("rhq.server.tomcat.security.truststore.type", serverProperties, true)); // keystore ssl.setCertificateKeyFile(buildExpression("rhq.server.tomcat.security.keystore.file", serverProperties, false)); ssl.setPassword(buildExpression("rhq.server.tomcat.security.keystore.password", serverProperties, true)); ssl.setKeyAlias(buildExpression("rhq.server.tomcat.security.keystore.alias", serverProperties, true)); ssl.setKeystoreType(buildExpression("rhq.server.tomcat.security.keystore.type", serverProperties, true)); // SSL protocol config ssl.setProtocol(buildExpression("rhq.server.tomcat.security.secure-socket-protocol", serverProperties, true)); ssl.setVerifyClient(buildExpression("rhq.server.tomcat.security.client-auth-mode", serverProperties, true)); // note: there doesn't appear to be a way for AS7 to support algorithm, like SunX509 or IbmX509 // so I think it just uses the JVM's default. This means "rhq.server.tomcat.security.algorithm" is unused ConnectorConfiguration connector = new ConnectorConfiguration(); connector.setMaxConnections(buildExpression("rhq.server.startup.web.max-connections", serverProperties, true)); connector.setScheme("https"); connector.setSocketBinding("https"); connector.setSslConfiguration(ssl); return connector; } /** * For a property whose value might be a file, return that file's absolute path. If the property * has a value whose pathname is already absolute, return it. If the property has a value whose path * is relative, it is considered relative to defaultRootDir and its absolute path based on that root dir * is returned. * * @param propertyName the property whose value in properties is considered a pathname (which may * relative or it may be absolute). * @param properties where to find the named property * @param defaultRootDir if the property value is a relative file path, this is what it is relative to * @return the absolute path of the file */ private static String getAbsoluteFileLocation(String propertyName, HashMap properties, String defaultRootDir) { if (properties == null || !properties.containsKey(propertyName)) { return null; } String propertyValue = properties.get(propertyName); File path = new File(propertyValue); if (path.isAbsolute()) { return path.getAbsolutePath(); } else { return new File(defaultRootDir, propertyValue).getAbsolutePath(); } } /** * You would think this would be simple - just set a value to ${propName:defaultVal} but some * JBossAS attributes don't support expressions when you think they could or should. In this * case, we'll pass in supportsExpression=false until we support AS versions that support * expressions (at which time we'll change to code to pass in true for those attributes). * If supportsExpression is true, the returned string will be ${propName} if defaultProperties is * null or doesn't have that property defined; ${propName:defaultVal} if the default property * is found. * If supportsExpression is false, this will take the actual property value for * propName found in the given default properties and return that string. * If there is no sysprop of that name, this method returns an empty string. * * @param propName * @param defaultProperties * @param supportsExpression * @return the attribute expression value (or real value if supportsExpression is false). */ private static String buildExpression(String propName, HashMap defaultProperties, boolean supportsExpression) { if (supportsExpression) { if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) { return "${" + propName + ":" + defaultProperties.get(propName) + "}"; } else { return "${" + propName + "}"; } } else { if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) { return defaultProperties.get(propName); } else { LOG.warn("There is no known value for property [" + propName + "]"); return ""; } } } /** * Creates a keystore whose cert has a CN of this server's public endpoint address. * * @param serverDetails details of the server being installed - must not be null and endpoint must be included in it * @param configDirStr location of a configuration directory where the keystore is to be stored * @return where the keystore file should be created (if an error occurs, this file won't exist) */ public static File createKeystore(ServerDetails serverDetails, String configDirStr) { File confDir = new File(configDirStr); File keystore = new File(confDir, "rhq.keystore"); File keystoreBackup = new File(confDir, "rhq.keystore.backup"); // if there is one out-of-box, we want to remove it and create one with our proper CN if (keystore.exists()) { keystoreBackup.delete(); if (!keystore.renameTo(keystoreBackup)) { LOG.warn("Cannot backup existing keystore - cannot generate a new cert with a proper domain name. [" + keystore + "] will be the keystore used by this server"); return keystore; } } try { String keystorePath = keystore.getAbsolutePath(); String keyAlias = "RHQ"; String domainName = "CN=" + serverDetails.getEndpointAddress() + ", OU=RHQ, O=rhq-project.org, C=US"; String keystorePassword = "RHQManagement"; String keyPassword = keystorePassword; String keyAlgorithm = "rsa"; int validity = 7300; SecurityUtil.createKeyStore(keystorePath, keyAlias, domainName, keystorePassword, keyPassword, keyAlgorithm, validity); LOG.info("New keystore created [" + keystorePath + "] with cert domain name of [" + domainName + "]"); } catch (Exception e) { LOG.warn("Could not generate a new cert with a proper domain name, will use the original keystore"); keystore.delete(); if (!keystoreBackup.renameTo(keystore)) { LOG.warn("Failed to restore the original keystore from backup - please rename [" + keystoreBackup + "] to [" + keystore + "]"); } } return keystore; } /** * Create an rhqadmin management user so when discovered, the AS7 plugin can use it to connect * to the RHQ Server. The password is set in rhq-server.properties. Because the plugin can't guess * the password, if not set to the default then the AS7 plugin will fail to connect, and the * RHQ Server resource connection properties will need to be updated after discovery and import. * * @param password the management password * @param serverDetails details of the server being installed * @param configDirStr location of a configuration directory where the mgmt-users.properties file lives */ public static void createDefaultManagementUser(String password, ServerDetails serverDetails, String configDirStr) { File confDir = new File(configDirStr); File mgmtUsers = new File(confDir, "mgmt-users.properties"); if (ServerInstallUtil.isEmpty(password)) { LOG.warn("Could not create default management user in file: [" + mgmtUsers + "] : invalid password [" + password + "]."); return; } // Add the default admin user, or if for some reason this file does not exist, just log the issue if (mgmtUsers.exists()) { try { PropertiesFileUpdate mgmtUsersPropFile = new PropertiesFileUpdate(mgmtUsers.getAbsolutePath()); Properties existingUsers = mgmtUsersPropFile.loadExistingProperties(); if (existingUsers.containsKey(RHQ_MGMT_USER)) { LOG.info("There is already a mgmt user named [" + RHQ_MGMT_USER + "], will not create another"); return; } } catch (Exception e) { LOG.warn("Cannot determine if mgmt user exists in [" + mgmtUsers + "]; will try to create it anyway", e); } FileOutputStream fos = null; try { String encodedPassword = new UsernamePasswordHashUtil().generateHashedHexURP(RHQ_MGMT_USER, "ManagementRealm", password.toCharArray()); fos = new FileOutputStream(mgmtUsers, true); fos.write(("\n" + RHQ_MGMT_USER + "=" + encodedPassword + "\n").getBytes()); } catch (Exception e) { LOG.warn("Could not create default management user in file: [" + mgmtUsers + "] : ", e); } finally { StreamUtil.safeClose(fos); } } else { LOG.warn("Could not create default management user. Could not find file: [" + mgmtUsers + "]"); } } public static void setSocketBindings(ModelControllerClient mcc, HashMap serverProperties) throws Exception { final SocketBindingJBossASClient client = new SocketBindingJBossASClient(mcc); for (SocketBindingInfo binding : defaultSocketBindings) { // use the port defined by the server's properties if set, otherwise, just use our hardcoded default int newPort = binding.port; String overrideValue = serverProperties.get(binding.sysprop); if (overrideValue != null) { try { newPort = Integer.parseInt(overrideValue); } catch (Exception e) { LOG.warn("Invalid port in system property [" + binding.sysprop + "]: " + overrideValue); } } LOG.info(String.format("Setting socket binding [%s] to [${%s:%d}]", binding.name, binding.sysprop, newPort)); try { client.setStandardSocketBindingPortExpression(binding.name, binding.sysprop, newPort); } catch (Exception e) { // If the binding is required, we re-throw a possible exception. Otherwise just log if (binding.required) { throw e; } else { LOG.info(String.format("Setting socket binding port for [%s] resulted in [%s] - this is harmless ", binding.name, e.getMessage())); // TODO log at debug level only? } } // if we need to switch the binding's interface, do it now if (binding.interfaceName != null) { LOG.info(String.format("Setting socket binding [%s] to use interface [%s]", binding.name, binding.interfaceName)); try { client.setStandardSocketBindingInterface(binding.name, binding.interfaceName); } catch (Exception e) { // If the binding is required, we re-throw a possible exception. Otherwise just log if (binding.required) { throw e; } else { LOG.info(String.format( "Setting socket binding interface for [%s] resulted in [%s] - this is harmless ", binding.name, e.getMessage())); // TODO log at debug level only? } } } } return; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy