package org.rhq.plugins.storage; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.yaml.snakeyaml.error.YAMLException; import org.rhq.cassandra.util.ConfigEditor; import org.rhq.cassandra.util.ConfigEditorException; 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.PropertySimple; import org.rhq.core.pluginapi.configuration.ConfigurationFacet; import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport; import org.rhq.core.pluginapi.operation.OperationFacet; import org.rhq.core.pluginapi.operation.OperationResult; import org.rhq.core.util.PropertiesFileUpdate; import org.rhq.core.util.StringUtil; /** * @author John Sanda */ public class StorageNodeConfigDelegate implements ConfigurationFacet { private Log log = LogFactory.getLog(StorageNodeConfigDelegate.class); private File jvmOptsFile; private File wrapperEnvFile; private File cassandraYamlFile; private OperationFacet invoker; public StorageNodeConfigDelegate(File basedir, OperationFacet invoker) { File confDir = new File(basedir, "conf"); jvmOptsFile = new File(confDir, "cassandra-jvm.properties"); cassandraYamlFile = new File(confDir, "cassandra.yaml"); this.invoker = invoker; // for windows, config props also get propagated to the wrapper env if (isWindows()) { File wrapperDir = new File(basedir, "../bin/wrapper"); wrapperEnvFile = new File(wrapperDir, "rhq-storage-wrapper.env"); } } @Override public Configuration loadResourceConfiguration() throws Exception { Properties properties = new Properties(); properties.load(new FileInputStream(jvmOptsFile)); Configuration config = new Configuration(); String heapDumpOnOOMError = properties.getProperty("heap_dump_on_OOMError"); String heapDumpDir = properties.getProperty("heap_dump_dir"); config.put(new PropertySimple("minHeapSize", getHeapMinProp(properties))); config.put(new PropertySimple("maxHeapSize", getHeapMaxProp(properties))); config.put(new PropertySimple("heapNewSize", getHeapNewProp(properties))); config.put(new PropertySimple("threadStackSize", getStackSizeProp(properties))); if (!StringUtil.isEmpty(heapDumpOnOOMError)) { config.put(new PropertySimple("heapDumpOnOOMError", true)); } else { config.put(new PropertySimple("heapDumpOnOOMError", false)); } if (!StringUtil.isEmpty(heapDumpDir)) { config.put(new PropertySimple("heapDumpDir", heapDumpDir)); } else { File basedir = jvmOptsFile.getParentFile().getParentFile(); config.put(new PropertySimple("heapDumpDir", new File(basedir, "bin").getAbsolutePath())); } ConfigEditor yamlEditor = new ConfigEditor(cassandraYamlFile); yamlEditor.load(); config.put(new PropertySimple("cqlPort", yamlEditor.getNativeTransportPort())); config.put(new PropertySimple("gossipPort", yamlEditor.getStoragePort())); // Read data directories here.. config.put(new PropertySimple("CommitLogLocation", yamlEditor.getCommitLogDirectory())); config.put(new PropertySimple("SavedCachesLocation", yamlEditor.getSavedCachesDirectory())); PropertyList dataFileLocations = new PropertyList("AllDataFileLocations"); for (String s : yamlEditor.getDataFileDirectories()) { dataFileLocations.add(new PropertySimple("directory", s)); } config.put(dataFileLocations); return config; } /** * Ensure that the path uses only forward slash. * @param path * @return forward-slashed path, or null if path is null */ private static String useForwardSlash(String path) { return (null != path) ? path.replace('\\', '/') : null; } private String getHeapMinProp(Properties properties) { String value = properties.getProperty("heap_min"); if (StringUtil.isEmpty(value)) { return ""; } if (!value.startsWith("-Xms")) { return value; } return value.substring(4); } private String getHeapMaxProp(Properties properties) { String value = properties.getProperty("heap_max"); if (StringUtil.isEmpty(value)) { return ""; } if (!value.startsWith("-Xmx")) { return value; } return value.substring(4); } private String getHeapNewProp(Properties properties) { String value = properties.getProperty("heap_new"); if (StringUtil.isEmpty(value)) { return ""; } if (!value.startsWith("-Xmn")) { return value; } return value.substring(4); } private String getStackSizeProp(Properties properties) { String value = properties.getProperty("thread_stack_size"); if (StringUtil.isEmpty(value)) { return ""; } if (!(value.startsWith("-Xss") || value.endsWith("k") || value.length() > 5)) { return value; } return value.substring(4, value.length() - 1); } @Override public void updateResourceConfiguration(ConfigurationUpdateReport configurationUpdateReport) { updateResourceConfigurationAndRestartIfNecessary(configurationUpdateReport, false); } public void updateResourceConfigurationAndRestartIfNecessary(ConfigurationUpdateReport configurationUpdateReport, boolean restartIfNecessary) { try { Configuration config = configurationUpdateReport.getConfiguration(); updateCassandraJvmProps(config); updateCassandraYaml(config); String dataFilesChangedString = config.getSimpleValue("dataDirectoriesChanged"); boolean dataFilesChanged = dataFilesChangedString != null && Boolean.parseBoolean(dataFilesChangedString); if(dataFilesChanged && invoker != null) { try { OperationResult moveDataFilesResult = invoker.invokeOperation("moveDataFiles", config); if(moveDataFilesResult.getErrorMessage() != null) { configurationUpdateReport.setErrorMessage(moveDataFilesResult.getErrorMessage()); } restartIfNecessary = false; // We have already restarted the storage node, don't do it twice } catch (Exception e) { configurationUpdateReport.setErrorMessage(e.getMessage()); } } if (isWindows()) { updateWrapperEnv(config); } configurationUpdateReport.setStatus(ConfigurationUpdateStatus.SUCCESS); } catch (IllegalArgumentException e) { configurationUpdateReport.setErrorMessage("No configuration update was applied: " + e.getMessage()); } catch (IOException e) { configurationUpdateReport.setErrorMessageFromThrowable(e); } catch (ConfigEditorException e) { configurationUpdateReport.setErrorMessageFromThrowable(e); } if (restartIfNecessary) { restartIfNecessary(configurationUpdateReport); } } private void restartIfNecessary(ConfigurationUpdateReport configurationUpdateReport) { boolean restartIsRequired = false; Configuration params = configurationUpdateReport.getConfiguration(); if (configurationUpdateReport.getStatus().equals(ConfigurationUpdateStatus.SUCCESS)) { if (params.getSimpleValue("maxHeapSize") != null || params.getSimpleValue("heapNewSize") != null || params.getSimpleValue("threadStackSize") != null) { restartIsRequired = true; } } if (restartIsRequired && invoker != null) { try { OperationResult restartResult = invoker.invokeOperation("restart", null); if (restartResult.getErrorMessage() != null) { configurationUpdateReport.setErrorMessage(restartResult.getErrorMessage()); } } catch (Exception e) { configurationUpdateReport.setErrorMessage(e.getMessage()); } } } private void updateCassandraJvmProps(Configuration newConfig) throws IOException { PropertiesFileUpdate propertiesUpdater = new PropertiesFileUpdate(jvmOptsFile.getAbsolutePath()); Properties properties = propertiesUpdater.loadExistingProperties(); String jmxPort = newConfig.getSimpleValue("jmxPort"); if (!StringUtil.isEmpty(jmxPort)) { validateIntegerArg("jmx_port", jmxPort); properties.setProperty("jmx_port", jmxPort); } String maxHeapSize = newConfig.getSimpleValue("maxHeapSize"); if (!StringUtil.isEmpty(maxHeapSize)) { validateHeapArg("maxHeapSize", maxHeapSize); // We want min and max heap to be the same properties.setProperty("heap_min", "-Xms" + maxHeapSize); properties.setProperty("heap_max", "-Xmx" + maxHeapSize); } String heapNewSize = newConfig.getSimpleValue("heapNewSize"); if (!StringUtil.isEmpty(heapNewSize)) { validateHeapArg("heapNewSize", heapNewSize); properties.setProperty("heap_new", "-Xmn" + heapNewSize); } String threadStackSize = newConfig.getSimpleValue("threadStackSize"); if (!StringUtil.isEmpty(threadStackSize)) { validateIntegerArg("threadStackSize", threadStackSize); properties.setProperty("thread_stack_size", "-Xss" + threadStackSize + "k"); } PropertySimple heapDumpOnOMMError = newConfig.getSimple("heapDumpOnOOMError"); if (heapDumpOnOMMError != null) { if (heapDumpOnOMMError.getBooleanValue()) { properties.setProperty("heap_dump_on_OOMError", "-XX:+HeapDumpOnOutOfMemoryError"); } else { properties.setProperty("heap_dump_on_OOMError", ""); } } String heapDumpDir = useForwardSlash(newConfig.getSimpleValue("heapDumpDir")); if (!StringUtil.isEmpty(heapDumpDir)) { properties.setProperty("heap_dump_dir", heapDumpDir); } propertiesUpdater.update(properties); } private void updateCassandraYaml(Configuration newConfig) { ConfigEditor editor = new ConfigEditor(cassandraYamlFile); try { editor.load(); PropertySimple cqlPortProperty = newConfig.getSimple("cqlPort"); if (cqlPortProperty != null) { editor.setNativeTransportPort(cqlPortProperty.getIntegerValue()); } PropertySimple gossipPortProperty = newConfig.getSimple("gossipPort"); if (gossipPortProperty != null) { editor.setStoragePort(gossipPortProperty.getIntegerValue()); } editor.save(); } catch (ConfigEditorException e) { if (e.getCause() instanceof YAMLException) { log.error("Failed to update " + cassandraYamlFile); log.info("Attempting to restore " + cassandraYamlFile); try { editor.restore(); throw e; } catch (ConfigEditorException e1) { log.error("Failed to restore " + cassandraYamlFile + ". A copy of the file prior to any " + "modifications can be found at " + editor.getBackupFile()); throw new ConfigEditorException("There was an error updating " + cassandraYamlFile + " and " + "undoing the changes failed. A copy of the file can be found at " + editor.getBackupFile() + ". See the agent logs for more details.", e); } } else { log.error("No updates were made to " + cassandraYamlFile + " due to an unexpected error", e); throw e; } } } private void updateWrapperEnv(Configuration config) throws IOException { PropertiesFileUpdate propertiesUpdater = new PropertiesFileUpdate(wrapperEnvFile.getAbsolutePath()); Properties properties = propertiesUpdater.loadExistingProperties(); String maxHeapSize = config.getSimpleValue("maxHeapSize"); if (!StringUtil.isEmpty(maxHeapSize)) { validateHeapArg("maxHeapSize", maxHeapSize); // We want min and max heap to be the same properties.setProperty("set.heap_min", "-Xms" + maxHeapSize); properties.setProperty("set.heap_max", "-Xmx" + maxHeapSize); } String heapNewSize = config.getSimpleValue("heapNewSize"); if (!StringUtil.isEmpty(heapNewSize)) { validateHeapArg("heapNewSize", heapNewSize); properties.setProperty("set.heap_new", "-Xmn" + heapNewSize); } String threadStackSize = config.getSimpleValue("threadStackSize"); if (!StringUtil.isEmpty(threadStackSize)) { validateIntegerArg("threadStackSize", threadStackSize); properties.setProperty("set.thread_stack_size", "-Xss" + threadStackSize + "k"); } PropertySimple heapDumpOnOMMError = config.getSimple("heapDumpOnOOMError"); if (heapDumpOnOMMError != null) { if (heapDumpOnOMMError.getBooleanValue()) { properties.setProperty("set.heap_dump_on_OOMError", "-XX:+HeapDumpOnOutOfMemoryError"); } else { properties.setProperty("set.heap_dump_on_OOMError", ""); } } String heapDumpDir = useForwardSlash(config.getSimpleValue("heapDumpDir")); if (!StringUtil.isEmpty(heapDumpDir)) { properties.setProperty("set.heap_dump_dir", "-XX:HeapDumpPath=" + heapDumpDir); } propertiesUpdater.update(properties); } private void validateHeapArg(String name, String value) { if (value.length() < 2) { throw new IllegalArgumentException(value + " is not a legal value for the property [" + name + "]"); } char[] chars = value.toCharArray(); for (int i = 0; i < chars.length - 1; ++i) { if (!Character.isDigit(chars[i])) { throw new IllegalArgumentException(value + " is not a legal value for the property [" + name + "]"); } } char lastChar = Character.toUpperCase(chars[chars.length - 1]); if (!(lastChar == 'M' || lastChar == 'G')) { throw new IllegalArgumentException(value + " is not a legal value for the property [" + name + "]"); } } private void validateIntegerArg(String name, String value) { try { Integer.parseInt(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(value + " is not a legal value for the property [" + name + "]"); } } private boolean isWindows() { return File.separatorChar == '\\'; } }