/*
* RHQ Management Platform
* Copyright (C) 2005-2014 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.plugins.postgres;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.sigar.ProcExe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ManualAddFacet;
import org.rhq.core.pluginapi.inventory.ProcessScanResult;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.ProcessInfo;
import org.rhq.core.system.SystemInfo;
import org.rhq.plugins.database.DatabasePluginUtil;
import org.rhq.plugins.postgres.util.PostgresqlConfFile;
/**
* @author Greg Hinkle
* @author Ian Springer
*/
public class PostgresDiscoveryComponent implements ResourceDiscoveryComponent, ManualAddFacet {
private static final Log LOG = LogFactory.getLog(PostgresDiscoveryComponent.class);
public static final String PGDATA_DIR_CONFIGURATION_PROPERTY = "pgdataDir";
public static final String CONFIG_FILE_CONFIGURATION_PROPERTY = "configFile";
public static final String DRIVER_CONFIGURATION_PROPERTY = "driverClass";
public static final String HOST_CONFIGURATION_PROPERTY = "host";
public static final String PORT_CONFIGURATION_PROPERTY = "port";
public static final String DB_CONFIGURATION_PROPERTY = "db";
public static final String PRINCIPAL_CONFIGURATION_PROPERTY = "principal";
public static final String CREDENTIALS_CONFIGURATION_PROPERTY = "credentials";
private static final String PGDATA_ENV_VAR = "PGDATA";
private static final String DEFAULT_RESOURCE_DESCRIPTION = "Postgres relational database server";
private static final String POSTGRES_DEFAULT_DATABASE_NAME = "postgres";
private static final Pattern VERSION_FROM_COMMANDLINE = Pattern.compile("\\d+(?:\\.\\d+)*");
@Override
public Set<DiscoveredResourceDetails> discoverResources(ResourceDiscoveryContext context) {
Set<DiscoveredResourceDetails> servers = new LinkedHashSet<DiscoveredResourceDetails>();
// Process any auto-discovered resources.
@SuppressWarnings("unchecked")
List<ProcessScanResult> autoDiscoveryResults = context.getAutoDiscoveredProcesses();
for (ProcessScanResult result : autoDiscoveryResults) {
LOG.info("Discovered a postgres process: " + result);
ProcessInfo procInfo = result.getProcessInfo();
String pgDataPath = getDataDirPath(procInfo);
if (pgDataPath == null) {
LOG.error("Unable to obtain data directory for postgres process with pid " + procInfo.getPid()
+ " (tried checking both -D command line argument, as well as " + PGDATA_ENV_VAR
+ " environment variable).");
continue;
}
File pgData = new File(pgDataPath);
String configFilePath = getConfigFilePath(procInfo);
PostgresqlConfFile confFile = null;
if (!pgData.exists()) {
if (LOG.isDebugEnabled()) {
LOG.debug("PostgreSQL data directory ("
+ pgData
+ ") does not exist or is not readable. "
+ "Make sure the user the RHQ Agent is running as has read permissions on the directory and its parent directory.");
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("PostgreSQL data directory: " + pgData);
}
File postgresConfFile = (configFilePath != null) ? new File(configFilePath) : new File(pgData,
PostgresServerComponent.DEFAULT_CONFIG_FILE_NAME);
if (LOG.isDebugEnabled()) {
LOG.debug("PostgreSQL configuration file: " + postgresConfFile);
}
if (!postgresConfFile.exists()) {
LOG.warn("PostgreSQL configuration file (" + postgresConfFile + ") does not exist.");
} else {
try {
confFile = new PostgresqlConfFile(postgresConfFile);
} catch (IOException e) {
LOG.warn("Could not load PostgreSQL configuration file [" + postgresConfFile + "]: " + e);
}
}
}
Configuration pluginConfig = context.getDefaultPluginConfiguration();
pluginConfig.put(new PropertySimple(PGDATA_DIR_CONFIGURATION_PROPERTY, pgData));
pluginConfig.put(new PropertySimple(CONFIG_FILE_CONFIGURATION_PROPERTY, configFilePath));
if (confFile != null) {
String port = confFile.getPort();
if (port != null) {
// Override the default (5432) from the descriptor.
pluginConfig.put(new PropertySimple(PORT_CONFIGURATION_PROPERTY, port));
}
List<String> listenAddresses = confFile.getPropertyList("listen_addresses");
if (listenAddresses.size() > 0) {
String listenAddress = listenAddresses.get(0).trim();
if ("*".equals(listenAddress)) {
listenAddress = "127.0.0.1";
}
pluginConfig.put(new PropertySimple(HOST_CONFIGURATION_PROPERTY, listenAddress));
}
}
DiscoveredResourceDetails resourceDetails = createResourceDetails(context, pluginConfig, procInfo, false);
servers.add(resourceDetails);
}
return servers;
/* TODO GH: Deal with the different error types and inventory except in case of connection refused
* Bad password org.postgresql.util.PSQLException: FATAL: password authentication failed for user "jon" Wrong
* port org.postgresql.util.PSQLException: Connection refused. Check that the hostname and port are correct and
* that the postmaster is accepting TCP/IP connections. Wrong db org.postgresql.util.PSQLException: FATAL:
* database "jon2" does not exist
*/
}
@Override
public DiscoveredResourceDetails discoverResource(Configuration pluginConfig,
ResourceDiscoveryContext discoveryContext) throws InvalidPluginConfigurationException {
return createResourceDetails(discoveryContext, pluginConfig, null, true);
}
protected static DiscoveredResourceDetails createResourceDetails(ResourceDiscoveryContext discoveryContext,
Configuration pluginConfiguration, @Nullable ProcessInfo processInfo, boolean logConnectionFailure) {
String key = buildUrl(pluginConfiguration);
Connection conn = null;
try {
conn = buildConnection(pluginConfiguration, logConnectionFailure);
String name = getServerResourceName(pluginConfiguration, conn);
String version = getVersion(processInfo, discoveryContext.getSystemInformation(), conn);
return new DiscoveredResourceDetails(discoveryContext.getResourceType(), key, name, version,
DEFAULT_RESOURCE_DESCRIPTION, pluginConfiguration, processInfo);
} finally {
DatabasePluginUtil.safeClose(conn);
}
}
protected static String buildUrl(Configuration config) {
String host = config.getSimple(HOST_CONFIGURATION_PROPERTY).getStringValue();
String port = config.getSimple(PORT_CONFIGURATION_PROPERTY).getStringValue();
String db = config.getSimple(DB_CONFIGURATION_PROPERTY).getStringValue();
return "jdbc:postgresql://" + host + ":" + port + "/" + db;
}
protected static String getVersion(ProcessInfo processInfo, SystemInfo systemInfo, Connection conn) {
String version = null;
try {
if (conn != null) {
version = conn.getMetaData().getDatabaseProductVersion();
}
} catch (SQLException e) {
// TODO GH: How to put this back to the server while inventorying this resource in an unconfigured state
LOG.info("Exception detecting postgres instance version.", e);
}
//now try to extract the version information by asking the server executable itself
if (version == null && processInfo != null) {
try {
ProcExe executable = processInfo.priorSnaphot().getExecutable();
if (executable != null) {
String postgresExe = executable.getName();
ProcessExecution execution = new ProcessExecution(postgresExe);
execution.setArguments(new String[] { "--version" });
execution.setCaptureOutput(true);
ProcessExecutionResults results = systemInfo.executeProcess(execution);
String versionInfo = results.getCapturedOutput();
Matcher m = VERSION_FROM_COMMANDLINE.matcher(versionInfo);
if (m.find()) {
version = versionInfo.substring(m.start(), m.end());
} else {
LOG.debug("Can't get the process executable - does the agent have the right permissions?");
}
}
} catch (Exception e) {
LOG.info("Failed to obtain Postgres version information from the executable file.", e);
}
}
return version;
}
public static Connection buildConnection(Configuration configuration, boolean logFailure) {
String driverClass = configuration.getSimple(DRIVER_CONFIGURATION_PROPERTY).getStringValue();
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
throw new InvalidPluginConfigurationException("Specified JDBC driver class (" + driverClass
+ ") not found.");
}
String url = buildUrl(configuration);
String principal = configuration.getSimple(PRINCIPAL_CONFIGURATION_PROPERTY).getStringValue();
String credentials = configuration.getSimple(CREDENTIALS_CONFIGURATION_PROPERTY).getStringValue();
try {
return DriverManager.getConnection(url, principal, credentials);
} catch (SQLException e) {
if (logFailure) {
LOG.info("Failed to connect to the database: " + e.getMessage());
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed to connect to the database: " + e.getMessage());
}
}
return null;
}
}
@Nullable
protected static String getDataDirPath(@NotNull ProcessInfo procInfo) {
String dataDirPath = null;
String[] cmdLine = procInfo.getCommandLine();
for (int i = 0; i < cmdLine.length; i++) {
if (cmdLine[i].equals("-D")) {
if (i != (cmdLine.length - 1)) {
dataDirPath = cmdLine[i + 1];
break;
} else {
LOG.error("-D option was last option on postgres command line: " + Arrays.asList(cmdLine));
}
}
}
if (dataDirPath == null) {
dataDirPath = procInfo.getEnvironmentVariable(PGDATA_ENV_VAR);
}
return dataDirPath;
}
@Nullable
private static String getConfigFilePath(@NotNull ProcessInfo procInfo) {
String configFilePath = null;
String[] cmdLine = procInfo.getCommandLine();
for (int i = 0; i < cmdLine.length; i++) {
if (cmdLine[i].equals("-c")) {
if (i != (cmdLine.length - 1)) {
String paramString = cmdLine[i + 1];
int equalsIndex = paramString.indexOf('=');
if (equalsIndex == -1) {
LOG.error("Invalid value '" + paramString + "' for -c option on postgres command line: "
+ Arrays.asList(cmdLine));
continue;
}
String paramName = paramString.substring(0, equalsIndex);
if (paramName.equalsIgnoreCase("config_file")) {
configFilePath = paramString.substring(equalsIndex + 1);
break;
}
} else {
LOG.error("-c option was last option on postgres command line: " + Arrays.asList(cmdLine));
}
}
}
return configFilePath;
}
private static List<String> getDatabaseNames(Connection conn) {
if (conn == null) {
return Collections.emptyList();
}
Statement statement = null;
ResultSet resultSet = null;
try {
List<String> ret = new ArrayList<String>();
statement = conn.createStatement();
resultSet = statement
.executeQuery("SELECT *, pg_database_size(datname) FROM pg_database where datistemplate = false");
while (resultSet.next()) {
String databaseName = resultSet.getString("datname");
ret.add(databaseName);
}
return ret;
} catch (SQLException e) {
LOG.error("Failed to obtain the list of databases in a postgres instance", e);
return Collections.emptyList();
} finally {
DatabasePluginUtil.safeClose(null, statement, resultSet);
}
}
private static String getServerResourceName(Configuration config, Connection conn) {
List<String> schemas = getDatabaseNames(conn);
if (schemas.size() > 0 && schemas.size() < 3) {
String firstDatabase = schemas.get(0);
String secondDatabase = schemas.get(1);
return POSTGRES_DEFAULT_DATABASE_NAME.equals(firstDatabase) ? secondDatabase : firstDatabase;
} else {
return config.getSimpleValue(DB_CONFIGURATION_PROPERTY, POSTGRES_DEFAULT_DATABASE_NAME);
}
}
}