/*
* 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.modules.plugins.wildfly10;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.rhq.core.pluginapi.bundle.BundleHandoverResponse.FailureType.EXECUTION;
import static org.rhq.core.pluginapi.bundle.BundleHandoverResponse.FailureType.INVALID_ACTION;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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.jetbrains.annotations.NotNull;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.measurement.AvailabilityType;
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.bundle.BundleHandoverRequest;
import org.rhq.core.pluginapi.bundle.BundleHandoverResponse;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
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.pluginapi.support.SnapshotReportRequest;
import org.rhq.core.pluginapi.support.SnapshotReportResults;
import org.rhq.core.pluginapi.support.SupportFacet;
import org.rhq.core.system.OperatingSystemType;
import org.rhq.modules.plugins.wildfly10.helper.AdditionalJavaOpts;
import org.rhq.modules.plugins.wildfly10.helper.JdrReportRunner;
import org.rhq.modules.plugins.wildfly10.helper.ServerPluginConfiguration;
import org.rhq.modules.plugins.wildfly10.json.Address;
import org.rhq.modules.plugins.wildfly10.json.CompositeOperation;
import org.rhq.modules.plugins.wildfly10.json.Operation;
import org.rhq.modules.plugins.wildfly10.json.PROPERTY_VALUE;
import org.rhq.modules.plugins.wildfly10.json.ReadAttribute;
import org.rhq.modules.plugins.wildfly10.json.ReadResource;
import org.rhq.modules.plugins.wildfly10.json.Result;
/**
* Component class for standalone AS7 servers.
*
* @author Heiko W. Rupp
*/
public class StandaloneASComponent<T extends ResourceComponent<?>> extends BaseServerComponent<T> implements
MeasurementFacet, OperationFacet, SupportFacet {
private static final Log LOG = LogFactory.getLog(StandaloneASComponent.class);
private static final String SERVER_CONFIG_TRAIT = "config-file";
private static final String MULTICAST_ADDRESS_TRAIT = "multicastAddress";
private static final String DEPLOY_DIR_TRAIT = "deployDir";
private static final String TEMP_DIR_TRAIT = "temp-dir";
private static final String JAVA_OPTS_ADDITIONAL_PROP = "javaOptsAdditional";
private static final Address ENVIRONMENT_ADDRESS = new Address("core-service=server-environment");
@Override
public void start(ResourceContext<T> resourceContext) throws Exception {
super.start(resourceContext);
updateAdditionalJavaOpts(resourceContext);
}
@Override
protected AS7Mode getMode() {
return AS7Mode.STANDALONE;
}
@Override
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) throws Exception {
Set<MeasurementScheduleRequest> leftovers = new HashSet<MeasurementScheduleRequest>(requests.size());
for (MeasurementScheduleRequest request : requests) {
String requestName = request.getName();
if (requestName.equals(SERVER_CONFIG_TRAIT)) {
collectConfigTrait(report, request);
} else if (requestName.equals(MULTICAST_ADDRESS_TRAIT)) {
collectMulticastAddressTrait(report, request);
} else if (requestName.equals(DEPLOY_DIR_TRAIT)) {
resolveDeployDir(report, request);
} else {
leftovers.add(request); // handled below
}
}
super.getValues(report, leftovers);
}
/**
* Try to determine the deployment directory (usually $as/standalone/deployments ).
* For JDG we return fake data, as JDG does not have such a directory.
* @param report Measurement report to tack the value on
* @param request Measurement request with the schedule id to use
*/
private void resolveDeployDir(MeasurementReport report, MeasurementScheduleRequest request) {
if ("JDG".equals(pluginConfiguration.getSimpleValue("productType", "AS7"))) {
LOG.debug("This is a JDG server, so there is no deployDir");
MeasurementDataTrait trait = new MeasurementDataTrait(request, "- not applicable to JDG -");
report.addData(trait);
return;
}
// So we have an AS7/EAP6
Address scanner = new Address("subsystem=deployment-scanner,scanner=default");
ReadResource op = new ReadResource(scanner);
Result res = getASConnection().execute(op);
if (res.isSuccess()) {
@SuppressWarnings("unchecked")
Map<String, String> scannerMap = (Map<String, String>) res.getResult();
String path = scannerMap.get("path");
String relativeTo = scannerMap.get("relative-to");
File basePath = resolveRelativePath(relativeTo);
// It is safe to use File.separator, as the agent we are running in, will also lay down the plugins
String deployDir = new File(basePath, path).getAbsolutePath();
MeasurementDataTrait trait = new MeasurementDataTrait(request, deployDir);
report.addData(trait);
} else {
LOG.error("No default deployment scanner was found, returning no value");
}
}
private File resolveRelativePath(String relativeTo) {
Address addr = new Address("path", relativeTo);
ReadResource op = new ReadResource(addr);
Result res = getASConnection().execute(op);
if (res.isSuccess()) {
@SuppressWarnings("unchecked")
Map<String, String> pathMap = (Map<String, String>) res.getResult();
String path = pathMap.get("path");
String relativeToProp = pathMap.get("relative-to");
if (relativeToProp == null)
return new File(path);
else {
File basePath = resolveRelativePath(relativeToProp);
return new File(basePath, path);
}
}
LOG.warn("The requested path property " + relativeTo + " is not registered in the server, so not resolving it.");
return new File(relativeTo);
}
@Override
protected Address getServerAddress() {
return getAddress();
}
@Override
protected String getSocketBindingGroup() {
// TODO (ips): Can this ever be something other than "standard-sockets"?
return "standard-sockets";
}
@Override
public OperationResult invokeOperation(String name, Configuration parameters) throws Exception {
if (name.equals("start")) {
return startServer();
} else if (name.equals("restart")) {
return restartServer(parameters);
} else if (name.equals("installRhqUser")) {
return installManagementUser(parameters, pluginConfiguration);
} else if (name.equals("executeCommands") || name.equals("executeScript")) {
return runCliCommand(parameters);
} else if (name.equals("setupCli")) {
return setupCli(parameters);
}
// reload, shutdown go to the remote server
Operation op = new Operation(name, new Address());
Result res = getASConnection().execute(op);
OperationResult operationResult = postProcessResult(name, res);
if (name.equals("shutdown")) {
if (waitUntilDown()) {
operationResult.setSimpleResult("Success");
} else {
operationResult.setErrorMessage("Was not able to shut down the server.");
}
}
if (name.equals("reload")) {
if (waitUntilReloaded()) {
operationResult.setSimpleResult("Success");
} else {
operationResult.setErrorMessage("Was not able to reload the server.");
}
}
context.getAvailabilityContext().requestAvailabilityCheck();
return operationResult;
}
private boolean waitUntilReloaded() throws InterruptedException {
boolean reloaded = false;
while (!reloaded) {
Operation op = new ReadAttribute(new Address(), "release-version");
try {
Result res = getASConnection().execute(op);
if (res.isSuccess() && !res.isReloadRequired()) {
reloaded = true;
}
} catch (Exception e) {
//do absolutely nothing
//if an exception is thrown that means the server is still reloading, so consider this
//a single failed attempt, equivalent to res.isSuccess == false
}
if (!reloaded) {
if (context.getComponentInvocationContext().isInterrupted()) {
// Operation canceled or timed out
throw new InterruptedException();
}
Thread.sleep(SECONDS.toMillis(1));
}
}
return reloaded;
}
@Override
public void updateResourceConfiguration(ConfigurationUpdateReport report) {
// We need to filter the path properties that are marked with the read-only flag
// This is done by setting the logical removed flag on the map to signal
// the write delegate to skip the map
Configuration config = report.getConfiguration();
PropertyList propertyList = config.getList("*3");
for (Property property : propertyList.getList()) {
PropertyMap map = (PropertyMap) property;
String ro = map.getSimpleValue("read-only", "false");
if (Boolean.parseBoolean(ro)) {
map.setErrorMessage(ConfigurationWriteDelegate.LOGICAL_REMOVED);
}
}
super.updateResourceConfiguration(report);
}
/**
* Handles content handed over during a bundle deployment.<br>
* <br>
* This component supports the following actions:<br>
* <br>
* <strong>action = deployment: deploys the content to the server</strong><br>
* <br>
* Optional parameters:<br>
* <ul>
* <li>runtimeName: Runtime name of the uploaded file (e.g. 'my.war'); if not present, the file name is used</li>
* </ul>
* <br>
* <strong>action = execute-script: executes a server CLI script</strong><br>
* <br>
* Optional parameters:<br>
* <ul>
* <li>waitTime (in seconds): how long to wait for completion; defaults to an hour</li>
* <li>killOnTimeout (true/false): should the CLI process be killed if timeout is reached; defaults to false</li>
* </ul>
*
* @param handoverRequest handover parameters and context
* @return a report object indicating success or failure
*/
@Override
public BundleHandoverResponse handleContent(BundleHandoverRequest handoverRequest) {
try {
if (handoverRequest.getAction().equals("deployment")) {
return handleDeployment(handoverRequest);
}
if (handoverRequest.getAction().equals("execute-script")) {
return handleExecuteScript(handoverRequest);
}
return BundleHandoverResponse.failure(INVALID_ACTION);
} catch (Exception e) {
return BundleHandoverResponse.failure(EXECUTION, "Unexpected handover failure", e);
}
}
private BundleHandoverResponse handleDeployment(BundleHandoverRequest request) {
// first make sure our server is UP. We need to check it, because this handover
// could happen right after "execute-script" handover, which could have reloaded the server
// @see https://bugzilla.redhat.com/show_bug.cgi?id=1252930
Integer timeout = BUNDLE_HANDOVER_SERVER_CHECK_TIMEOUT;
String waitForServer = request.getParams().get(BUNDLE_HANDOVER_SERVER_CHECK_TIMEOUT_PARAM);
if(waitForServer != null && waitForServer.length() > 0) {
try {
timeout = Integer.valueOf(waitForServer);
} catch(NumberFormatException e) {
return BundleHandoverResponse.failure(EXECUTION,
"Given server timeout parameter is not a valid number: " + waitForServer);
}
}
if (!ensureServerUp(timeout)) { // Value 0 disables the check
return BundleHandoverResponse.failure(EXECUTION,
"Failed to upload deployment content, " + this.context.getResourceDetails()
+ " is currently not responding or " + AvailabilityType.DOWN);
}
HandoverContentUploader contentUploader = new HandoverContentUploader(request, getASConnection());
boolean uploaded = contentUploader.upload();
if (!uploaded) {
return contentUploader.getFailureResponse();
}
String filename = contentUploader.getFilename();
String runtimeName = contentUploader.getRuntimeName();
String hash = contentUploader.getHash();
Redeployer redeployer = new Redeployer(runtimeName, hash, getASConnection());
if (redeployer.deploymentExists()) {
Result result = redeployer.redeployOnServer();
if (result.isRolledBack()) {
return BundleHandoverResponse.failure(EXECUTION, result.getFailureDescription());
}
return BundleHandoverResponse.success();
}
Operation addDeploymentStep = new Operation("add", "deployment", filename);
List<Object> addDeploymentContentProperty = new ArrayList<Object>(1);
Map<String, Object> contentValues = new HashMap<String, Object>();
contentValues.put("hash", new PROPERTY_VALUE("BYTES_VALUE", hash));
addDeploymentContentProperty.add(contentValues);
addDeploymentStep.addAdditionalProperty("content", addDeploymentContentProperty);
addDeploymentStep.addAdditionalProperty("name", filename);
addDeploymentStep.addAdditionalProperty("runtime-name", runtimeName);
Operation deployStep = new Operation("deploy", addDeploymentStep.getAddress());
CompositeOperation compositeOperation = new CompositeOperation();
compositeOperation.addStep(addDeploymentStep);
compositeOperation.addStep(deployStep);
Result result = getASConnection().execute(compositeOperation, 300);
if (!result.isSuccess()) {
return BundleHandoverResponse.failure(EXECUTION, result.getFailureDescription());
} else {
return BundleHandoverResponse.success();
}
}
@NotNull
@Override
protected Address getEnvironmentAddress() {
return ENVIRONMENT_ADDRESS;
}
@NotNull
@Override
protected Address getHostAddress() {
// In standalone mode, the root address is the host address.
return getAddress();
}
@NotNull
@Override
protected String getBaseDirAttributeName() {
return "base-dir";
}
@NotNull
@Override
protected String getConfigDirAttributeName() {
return "config-dir";
}
@Override
protected String getTempDirAttributeName() {
return TEMP_DIR_TRAIT;
}
/**
* Updates JAVA_OPTS in standalone.conf and standalone.conf.bat files.
* If JAVA_OPTS is set, then new config is added or updated in the file
* If JAVA_OPTS is unset, then the config file will be cleared of any traced the config set via RHQ
*/
private void updateAdditionalJavaOpts(ResourceContext<T> resourceContext) {
if (resourceContext.getPluginConfiguration().getSimpleValue(ServerPluginConfiguration.Property.HOME_DIR) == null) {
LOG.error("Additional JAVA_OPTS cannot be configured because "
+ ServerPluginConfiguration.Property.HOME_DIR + " property not set");
return;
}
File baseDirectory = new File(resourceContext.getPluginConfiguration().getSimpleValue(
ServerPluginConfiguration.Property.HOME_DIR));
File binDirectory = new File(baseDirectory, "bin");
String additionalJavaOptsContent = resourceContext.getPluginConfiguration().getSimpleValue(
JAVA_OPTS_ADDITIONAL_PROP);
File configFile;
AdditionalJavaOpts additionalJavaOptsConfig;
if (OperatingSystemType.WINDOWS.equals(resourceContext.getSystemInformation().getOperatingSystemType())) {
configFile = new File(binDirectory, "standalone.conf.bat");
additionalJavaOptsConfig = new AdditionalJavaOpts.WindowsConfiguration();
} else {
configFile = new File(binDirectory, "standalone.conf");
additionalJavaOptsConfig = new AdditionalJavaOpts.LinuxConfiguration();
}
try {
if (additionalJavaOptsContent != null && !additionalJavaOptsContent.trim().isEmpty()) {
additionalJavaOptsConfig.update(configFile, additionalJavaOptsContent);
} else {
additionalJavaOptsConfig.clean(configFile);
}
} catch (Exception e) {
LOG.error("Unable to update configuration file with additional JAVA_OPTS set via RHQ.", e);
}
}
@Override
public SnapshotReportResults getSnapshotReport(SnapshotReportRequest request) throws Exception {
if (AvailabilityType.UP.equals(getAvailability())) {
if ("jdr".equals(request.getName())) {
InputStream is = new JdrReportRunner(getServerAddress(), getASConnection()).getReport();
return new SnapshotReportResults(is);
}
return null;
}
throw new Exception("Cannot obtain report, resource is not UP");
}
}