/* * 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 static org.rhq.core.domain.measurement.AvailabilityType.DOWN; import static org.rhq.core.domain.measurement.AvailabilityType.UP; import static org.rhq.core.domain.resource.CreateResourceStatus.FAILURE; import static org.rhq.core.domain.resource.CreateResourceStatus.INVALID_CONFIGURATION; import static org.rhq.core.domain.resource.CreateResourceStatus.SUCCESS; import static org.rhq.plugins.database.DatabasePluginUtil.safeClose; import static org.rhq.plugins.postgres.PostgresUserComponent.ResourceConfig.CAN_UPDATE_SYSTEM_CATALOGS_DIRECTLY; import static org.rhq.plugins.postgres.PostgresUserComponent.ResourceConfig.SUPERUSER; import static org.rhq.plugins.postgres.PostgresUserComponent.UPDATE_PG_AUTHID_SET_ROLCATUPDATE_WHERE_OID; import static org.rhq.plugins.postgres.PostgresUserComponent.buildUserSql; import static org.rhq.plugins.postgres.PostgresUserDiscoveryComponent.createResourceKey; import static org.rhq.plugins.postgres.PostgresUserDiscoveryComponent.getUserOid; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.File; import java.io.IOException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.ConfigurationUpdateStatus; import org.rhq.core.domain.configuration.PropertyList; import org.rhq.core.domain.configuration.PropertyMap; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.configuration.definition.ConfigurationDefinition; import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple; import org.rhq.core.domain.configuration.definition.PropertySimpleType; import org.rhq.core.domain.measurement.AvailabilityType; import org.rhq.core.domain.measurement.MeasurementDataNumeric; import org.rhq.core.domain.measurement.MeasurementDataTrait; import org.rhq.core.domain.measurement.MeasurementReport; import org.rhq.core.domain.measurement.MeasurementScheduleRequest; import org.rhq.core.pluginapi.configuration.ConfigurationFacet; import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport; import org.rhq.core.pluginapi.inventory.CreateChildResourceFacet; import org.rhq.core.pluginapi.inventory.CreateResourceReport; import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException; import org.rhq.core.pluginapi.inventory.ResourceComponent; import org.rhq.core.pluginapi.inventory.ResourceContext; import org.rhq.core.pluginapi.measurement.MeasurementFacet; import org.rhq.core.pluginapi.operation.OperationFacet; import org.rhq.core.pluginapi.operation.OperationResult; import org.rhq.core.system.AggregateProcessInfo; import org.rhq.core.system.ProcessInfo; import org.rhq.plugins.database.ConnectionPoolingSupport; import org.rhq.plugins.database.DatabaseComponent; import org.rhq.plugins.database.DatabasePluginUtil; import org.rhq.plugins.database.PooledConnectionProvider; import org.rhq.plugins.postgres.util.PostgresqlConfFile; /** * Management for a PostgreSQL server * * @author Greg Hinkle */ public class PostgresServerComponent<T extends ResourceComponent<?>> implements DatabaseComponent<T>, ConnectionPoolingSupport, ConfigurationFacet, MeasurementFacet, OperationFacet, CreateChildResourceFacet { private static final Log LOG = LogFactory.getLog(PostgresServerComponent.class); private static final String METRIC_RUNTIME_PREFIX = "Runtime."; // See http://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.2#pg_stat_activity_and_pg_stat_replication.27s_definitions_have_changed private static final String FIND_STAT_ACTIVITY = "select " // + "pid, usename, query, state, client_addr, client_port " // + "from pg_stat_activity order by pid asc"; private static final String FIND_STAT_ACTIVITY_PRE_PG_9_2 = "select " // + "procpid as pid, usename, current_query as query, " // + "(case when current_query='<IDLE>' then 'idle' else 'active' end) as state, " // + "client_addr, client_port " // + "from pg_stat_activity order by pid asc"; static final String DEFAULT_CONFIG_FILE_NAME = "postgresql.conf"; private AggregateProcessInfo aggregateProcessInfo; @Deprecated private Connection connection; private ResourceContext resourceContext; private PostgresPooledConnectionProvider pooledConnectionProvider; @Override public void start(ResourceContext context) throws Exception { this.resourceContext = context; buildSharedConnectionIfNeeded(); try { pooledConnectionProvider = new PostgresPooledConnectionProvider(resourceContext.getPluginConfiguration()); } catch (SQLException e) { if (e.getCause() instanceof SQLException) { SQLException cause = (SQLException) e.getCause(); if ("28P01".equals(cause.getSQLState())) { throw new InvalidPluginConfigurationException("Invalid password"); } } throw new InvalidPluginConfigurationException("Cannot open database connection", e); } ProcessInfo processInfo = resourceContext.getNativeProcess(); if (processInfo != null) { aggregateProcessInfo = processInfo.getAggregateProcessTree(); } else { findProcessInfo(); } File configFile = getConfigurationFileObject(); if (!configFile.exists()) { LOG.warn("PostgreSQL configuration file [" + configFile + "] does not exist or is not readable. " + "Make sure the user the RHQ Agent is running as has read permissions on the file " + "and its parent directory."); } else if (!configFile.canRead()) { LOG.warn("PostgreSQL configuration file [" + configFile + "] is not readable. " + "Make sure the user the RHQ Agent is running as has read permissions on the file."); } } @Override public void stop() { resourceContext = null; DatabasePluginUtil.safeClose(connection); connection = null; pooledConnectionProvider.close(); pooledConnectionProvider = null; aggregateProcessInfo = null; } @Override public boolean supportsConnectionPooling() { return true; } @Override public PooledConnectionProvider getPooledConnectionProvider() { return pooledConnectionProvider; } protected String getJDBCUrl() { return PostgresDiscoveryComponent.buildUrl(resourceContext.getPluginConfiguration()); } @Override public AvailabilityType getAvailability() { Connection jdbcConnection = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); return jdbcConnection.isValid(1) ? UP : DOWN; } catch (SQLException e) { return DOWN; } finally { DatabasePluginUtil.safeClose(jdbcConnection); } } ResourceContext getResourceContext() { return resourceContext; } @Override public Connection getConnection() { buildSharedConnectionIfNeeded(); return connection; } private void buildSharedConnectionIfNeeded() { try { if ((connection == null) || connection.isClosed()) { connection = PostgresDiscoveryComponent.buildConnection(this.resourceContext.getPluginConfiguration(), true); } } catch (SQLException e) { if (LOG.isDebugEnabled()) { LOG.debug("Could not build shared connection", e); } } } @Override public void removeConnection() { DatabasePluginUtil.safeClose(this.connection); this.connection = null; } // TODO: Why are we only supporting this small subset of config file params? (ips, 10/4/07) private static final String[] CONFIG_FILE_PROPERTIES = { "port", "max_connections", "shared_buffers", "max_fsm_pages", "log_destination", "redirect_stderr", "stats_start_collector", "stats_block_level", "stats_row_level", "autovacuum" }; protected PostgresqlConfFile getConfigurationFile() throws IOException { File configFile = getConfigurationFileObject(); return new PostgresqlConfFile(configFile); } private File getConfigurationFileObject() { Configuration pluginConfig = resourceContext.getPluginConfiguration(); String dataDirPath = pluginConfig.getSimpleValue(PostgresDiscoveryComponent.PGDATA_DIR_CONFIGURATION_PROPERTY, null); String configFilePath = pluginConfig.getSimpleValue( PostgresDiscoveryComponent.CONFIG_FILE_CONFIGURATION_PROPERTY, null); return (configFilePath != null) ? new File(configFilePath) : new File(dataDirPath, DEFAULT_CONFIG_FILE_NAME); } @Override public Configuration loadResourceConfiguration() throws Exception { Configuration config = new Configuration(); ConfigurationDefinition configDef = resourceContext.getResourceType().getResourceConfigurationDefinition(); // Persisted settings - obtained by reading postgresql.conf. PostgresqlConfFile confFile = getConfigurationFile(); for (String propName : CONFIG_FILE_PROPERTIES) { String value = confFile.getProperty(propName); PropertyDefinitionSimple propDef = configDef.getPropertyDefinitionSimple(propName); PropertySimple prop = createProperty(value, propDef); config.put(prop); } return config; } @Override public void updateResourceConfiguration(ConfigurationUpdateReport report) { try { ConfigurationDefinition def = resourceContext.getResourceType().getResourceConfigurationDefinition(); Map<String, String> parameters = new HashMap<String, String>(); for (PropertySimple prop : report.getConfiguration().getSimpleProperties().values()) { PropertyDefinitionSimple pd = def.getPropertyDefinitionSimple(prop.getName()); if ("configFile".equals(pd.getPropertyGroupDefinition().getName())) { // configuration file String value = getPostgresParameterValue(prop, pd); parameters.put(prop.getName(), value); } } PostgresqlConfFile confFile = getConfigurationFile(); confFile.setProperties(parameters); } catch (IOException e) { LOG.error("Unable to update postgres configuration file", e); } report.setStatus(ConfigurationUpdateStatus.SUCCESS); } /** * Get data about the database server. Currently we have two categories: * <ul> * <li>Database.* are metrics that are obtained from the database server itself</li> * <li>Process.* are metrics obtained from the native system.</li> * </ul> * * @param report the report where all collected measurement data will be added * @param metrics the schedule of what needs to be collected when */ @Override public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> metrics) { Map<String, MeasurementScheduleRequest> runtimePropertiesRequests = new HashMap<String, MeasurementScheduleRequest>( metrics.size()); for (MeasurementScheduleRequest request : metrics) { String metricName = request.getName(); if (metricName.startsWith("Process.")) { if (aggregateProcessInfo != null) { aggregateProcessInfo.refresh(); //report.addData(new MeasurementDataNumeric(request, getProcessProperty(request.getName()))); Object val = lookupAttributeProperty(aggregateProcessInfo, metricName.substring("Process.".length())); if (val != null && val instanceof Number) { // aggregateProcessInfo.getAggregateMemory().Cpu().getTotal() report.addData(new MeasurementDataNumeric(request, ((Number) val).doubleValue())); } } } else if (metricName.startsWith("Database")) { if (metricName.endsWith("startTime")) { // db start time Connection jdbcConnection = null; Statement statement = null; ResultSet resultSet = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); statement = jdbcConnection.createStatement(); resultSet = statement.executeQuery("SELECT pg_postmaster_start_time()"); if (resultSet.next()) { report.addData(new MeasurementDataTrait(request, resultSet.getTimestamp(1).toString())); } } catch (SQLException e) { if (LOG.isDebugEnabled()) { LOG.debug("Can not collect metric: " + metricName + ": " + e.getLocalizedMessage()); } } finally { DatabasePluginUtil.safeClose(jdbcConnection, statement, resultSet); } } else if (metricName.endsWith("backends")) { // number of connected backends Connection jdbcConnection = null; Statement statement = null; ResultSet resultSet = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); statement = jdbcConnection.createStatement(); resultSet = statement.executeQuery("select count(*) from pg_stat_activity"); if (resultSet.next()) { report.addData(new MeasurementDataNumeric(request, (double) resultSet.getLong(1))); } } catch (SQLException e) { if (LOG.isDebugEnabled()) { LOG.debug("Can not collect metricName: " + metricName + ": " + e.getLocalizedMessage()); } } finally { DatabasePluginUtil.safeClose(jdbcConnection, statement, resultSet); } } } else if (metricName.startsWith(METRIC_RUNTIME_PREFIX)) { runtimePropertiesRequests.put(metricName.substring(METRIC_RUNTIME_PREFIX.length()), request); } } if (!runtimePropertiesRequests.isEmpty()) { Connection jdbcConnection = null; Statement statement = null; ResultSet resultSet = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); statement = jdbcConnection.createStatement(); resultSet = statement.executeQuery("show all"); while (resultSet.next()) { String runtimeProperty = resultSet.getString("name"); if (!runtimePropertiesRequests.containsKey(runtimeProperty)) { continue; } String setting = resultSet.getString("setting"); MeasurementScheduleRequest request = runtimePropertiesRequests.get(runtimeProperty); switch (request.getDataType()) { case TRAIT: report.addData(new MeasurementDataTrait(request, setting)); break; default: if (LOG.isDebugEnabled()) { LOG.debug("Unsupported metric data type: " + request.getName() + ", " + request.getDataType()); } } } } catch (SQLException e) { LOG.debug("Can not collect metrics: " + runtimePropertiesRequests.keySet() + ": " + e.getLocalizedMessage()); } finally { DatabasePluginUtil.safeClose(jdbcConnection, statement, resultSet); } } } protected Object lookupAttributeProperty(Object value, String property) { String[] ps = property.split("\\.", 2); String searchProperty = ps[0]; // Try to use reflection try { PropertyDescriptor[] pds = Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { if (pd.getName().equals(searchProperty)) { value = pd.getReadMethod().invoke(value); } } } catch (Exception e) { if (LOG.isDebugEnabled()) { LOG.debug("Unable to read property from measurement attribute [" + searchProperty + "] not found on [" + this.resourceContext.getResourceKey() + "]"); } } if (ps.length > 1) { value = lookupAttributeProperty(value, ps[1]); } return value; } /** * @deprecated since RHQ4.13, unused */ @Deprecated public double getObjectProperty(Object object, String name) { try { BeanInfo info = Introspector.getBeanInfo(object.getClass()); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { if (pd.getName().equals(name)) { return ((Number) pd.getReadMethod().invoke(object)).doubleValue(); } } } catch (Exception e) { LOG.error("Error occurred while retrieving property '" + name + "' from object [" + object + "]", e); } return Double.NaN; } @Override public OperationResult invokeOperation(String name, Configuration parameters) throws Exception { if (name.equals("listProcessStatistics")) { Connection jdbcConnection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); if (isVersionGreaterThan92(jdbcConnection)) { statement = jdbcConnection.prepareStatement(FIND_STAT_ACTIVITY); } else { statement = jdbcConnection.prepareStatement(FIND_STAT_ACTIVITY_PRE_PG_9_2); } resultSet = statement.executeQuery(); PropertyList procList = new PropertyList("processList"); while (resultSet.next()) { PropertyMap pm = new PropertyMap("process"); pm.put(new PropertySimple("pid", resultSet.getInt("pid"))); pm.put(new PropertySimple("userName", resultSet.getString("usename"))); pm.put(new PropertySimple("query", resultSet.getString("query"))); pm.put(new PropertySimple("state", resultSet.getString("state"))); pm.put(new PropertySimple("address", resultSet.getString("client_addr"))); pm.put(new PropertySimple("port", resultSet.getInt("client_port"))); procList.add(pm); } OperationResult result = new OperationResult(); result.getComplexResults().put(procList); return result; } finally { DatabasePluginUtil.safeClose(jdbcConnection, statement, resultSet); } } return null; } @Override public CreateResourceReport createResource(CreateResourceReport report) { Configuration userConfig = report.getResourceConfiguration(); String user = userConfig.getSimpleValue("user"); if (user == null || user.trim().isEmpty()) { report.setStatus(FAILURE); report.setErrorMessage("User name is missing"); return report; } Connection jdbcConnection = null; PreparedStatement statement = null; try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); statement = jdbcConnection.prepareStatement(buildUserSql(userConfig, /*new user*/true)); // NOTE: Postgres doesn't seem to indicate the expect count of 1 row updated but this work // Postgres returns 0 for DDL that does not return rows statement.executeUpdate(); } catch (SQLException e) { report.setStatus(FAILURE); report.setErrorMessage("Failed to create user."); report.setException(e); return report; } finally { DatabasePluginUtil.safeClose(jdbcConnection, statement); } String resourceName = report.getUserSpecifiedResourceName(); if (resourceName == null || resourceName.trim().isEmpty()) { resourceName = user; } report.setResourceName(resourceName); long userOid; try { userOid = getUserOid(user, getPooledConnectionProvider()); report.setResourceKey(createResourceKey(userOid)); } catch (SQLException e) { report.setStatus(FAILURE); report.setErrorMessage("The user has been created but its oid could not be read."); report.setException(e); return report; } try { jdbcConnection = getPooledConnectionProvider().getPooledConnection(); statement = jdbcConnection.prepareStatement(UPDATE_PG_AUTHID_SET_ROLCATUPDATE_WHERE_OID); statement.setBoolean( 1, Boolean.valueOf(userConfig.getSimpleValue(SUPERUSER)) && Boolean.valueOf(userConfig.getSimpleValue(CAN_UPDATE_SYSTEM_CATALOGS_DIRECTLY))); statement.setLong(2, userOid); statement.executeUpdate(); } catch (SQLException e) { report.setStatus(INVALID_CONFIGURATION); report.setErrorMessage("The user has been created but cannot modify system catalogs directly."); report.setException(e); return report; } finally { safeClose(jdbcConnection, statement); } report.setStatus(SUCCESS); return report; } private String getPostgresParameterValue(PropertySimple prop, PropertyDefinitionSimple propDef) { String value; if ((propDef.getType() == PropertySimpleType.BOOLEAN) && (prop.getBooleanValue() != null)) { //noinspection ConstantConditions value = (prop.getBooleanValue()) ? "on" : "off"; } else { value = prop.getStringValue(); } return value; } private PropertySimple createProperty(String value, PropertyDefinitionSimple propDef) { String jonValue; if ((propDef.getType() == PropertySimpleType.BOOLEAN) && (value != null)) { String lowerCaseValue = value.toLowerCase(); if ("on".equals(lowerCaseValue) || "true".startsWith(lowerCaseValue) || "yes".startsWith(lowerCaseValue) || "1".equals(lowerCaseValue)) { jonValue = Boolean.TRUE.toString(); } else if (("off".startsWith(lowerCaseValue) && (lowerCaseValue.length() != 1)) || "false".startsWith(lowerCaseValue) || "no".startsWith(lowerCaseValue) || "0".equals(lowerCaseValue)) { jonValue = Boolean.FALSE.toString(); } else { jonValue = (propDef.isRequired()) ? Boolean.FALSE.toString() : null; LOG.warn("Boolean PostgreSQL configuration parameter '" + propDef.getName() + "' has an invalid value: '" + value + "' - defaulting value to '" + jonValue + "'"); } } else { jonValue = value; } return new PropertySimple(propDef.getName(), jonValue); } public void findProcessInfo() { List<ProcessInfo> processes = this.resourceContext .getSystemInformation() .getProcesses( "process|basename|match=^(?i)(postgres|postmaster)\\.exe$,process|basename|nomatch|parent=^(?i)(postgres|postmaster)\\.exe$"); processes.addAll(this.resourceContext.getSystemInformation().getProcesses( "process|basename|match=^(postgres|postmaster)$,process|basename|nomatch|parent=^(postgres|postmaster)$")); for (ProcessInfo processInfo : processes) { String pgDataPath = PostgresDiscoveryComponent.getDataDirPath(processInfo); if (pgDataPath != null) { this.aggregateProcessInfo = processInfo.getAggregateProcessTree(); break; } } } /** * Tests Postgres version. * * @return true if Postgres version is greater than 9.2, false otherwise * @throws SQLException */ boolean isVersionGreaterThan92() throws SQLException { Connection connection = null; try { connection = getPooledConnectionProvider().getPooledConnection(); return isVersionGreaterThan92(connection); } finally { safeClose(connection); } } /** * Tests Postgres version. This method does not call {@link java.sql.Connection#close()} on the provided * <code>connection</code>. * * @param connection a JDBC connection * @return true if Postgres version is greater than 9.2, false otherwise * @throws SQLException */ boolean isVersionGreaterThan92(Connection connection) throws SQLException { DatabaseMetaData metaData = connection.getMetaData(); return metaData.getDatabaseMajorVersion() >= 9 && metaData.getDatabaseMinorVersion() >= 2; } boolean isVersionGreaterThanOrEqualTo90(Connection connection) throws SQLException { DatabaseMetaData metaData = connection.getMetaData(); return metaData.getDatabaseMajorVersion() >= 9; } }