/*
* 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.ant;
import static org.rhq.core.domain.bundle.BundleResourceDeploymentHistory.Category.AUDIT_MESSAGE;
import static org.rhq.core.domain.bundle.BundleResourceDeploymentHistory.Category.DEPLOY_STEP;
import static org.rhq.core.domain.bundle.BundleResourceDeploymentHistory.Status.FAILURE;
import static org.rhq.core.domain.bundle.BundleResourceDeploymentHistory.Status.SUCCESS;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.rhq.bundle.ant.AntLauncher;
import org.rhq.bundle.ant.BundleAntProject;
import org.rhq.bundle.ant.DeployPropertyNames;
import org.rhq.bundle.ant.DeploymentPhase;
import org.rhq.bundle.ant.HandoverTarget;
import org.rhq.bundle.ant.InvalidBuildFileException;
import org.rhq.bundle.ant.LoggerAntBuildListener;
import org.rhq.bundle.ant.type.HandoverInfo;
import org.rhq.core.domain.bundle.BundleDeployment;
import org.rhq.core.domain.bundle.BundleResourceDeployment;
import org.rhq.core.domain.bundle.BundleVersion;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.tagging.Tag;
import org.rhq.core.pluginapi.bundle.BundleDeployRequest;
import org.rhq.core.pluginapi.bundle.BundleDeployResult;
import org.rhq.core.pluginapi.bundle.BundleFacet;
import org.rhq.core.pluginapi.bundle.BundleHandoverContext;
import org.rhq.core.pluginapi.bundle.BundleHandoverRequest;
import org.rhq.core.pluginapi.bundle.BundleHandoverResponse;
import org.rhq.core.pluginapi.bundle.BundleManagerProvider;
import org.rhq.core.pluginapi.bundle.BundlePurgeRequest;
import org.rhq.core.pluginapi.bundle.BundlePurgeResult;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.system.SystemInfoFactory;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.FileUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.core.util.updater.DeployDifferences;
import org.rhq.core.util.updater.DeploymentsMetadata;
import org.rhq.core.util.updater.DestinationComplianceMode;
import org.rhq.core.util.updater.FileHashcodeMap;
/**
* @author John Mazzitelli
*/
@SuppressWarnings("unchecked")
public class AntBundlePluginComponent implements ResourceComponent, BundleFacet {
private static final Log LOG = LogFactory.getLog(AntBundlePluginComponent.class);
private File tmpDirectory;
@Override
public void start(ResourceContext context) throws Exception {
this.tmpDirectory = new File(context.getTemporaryDirectory(), "ant-bundle-plugin");
this.tmpDirectory.mkdirs();
if (!this.tmpDirectory.exists() || !this.tmpDirectory.isDirectory()) {
throw new Exception("Failed to create tmp dir [" + this.tmpDirectory + "] - cannot process Ant bundles.");
}
}
@Override
public void stop() {
}
@Override
public AvailabilityType getAvailability() {
return AvailabilityType.UP;
}
@Override
public BundleDeployResult deployBundle(final BundleDeployRequest request) {
BundleDeployResult result = new BundleDeployResult();
try {
BundleResourceDeployment resourceDeployment = request.getResourceDeployment();
BundleDeployment bundleDeployment = resourceDeployment.getBundleDeployment();
BundleVersion bundleVersion = bundleDeployment.getBundleVersion();
String recipe = bundleVersion.getRecipe();
File recipeFile = File.createTempFile("ant-bundle-recipe", ".xml", request.getBundleFilesLocation());
File logFile = File.createTempFile("ant-bundle-recipe", ".log", this.tmpDirectory);
PrintWriter logFileOutput = null;
try {
// Open the log file for writing.
logFileOutput = new PrintWriter(new FileOutputStream(logFile, true));
// Store the recipe in the tmp recipe file.
ByteArrayInputStream in = new ByteArrayInputStream(recipe.getBytes());
FileOutputStream out = new FileOutputStream(recipeFile);
StreamUtil.copy(in, out);
// Get the bundle's configuration values and the global system facts and
// add them as Ant properties so the Ant script can get their values.
Properties antProps = createAntProperties(request);
// TODO: Eventually the phase to be executed should be passed in by the PC when it calls us.
// TODO: Invoke STOP phase.
// TODO: Invoke START phase.
List<BuildListener> buildListeners = new ArrayList();
LoggerAntBuildListener logger = new LoggerAntBuildListener(null, logFileOutput, Project.MSG_DEBUG);
buildListeners.add(logger);
DeploymentAuditorBuildListener auditor = new DeploymentAuditorBuildListener(
request.getBundleManagerProvider(), resourceDeployment);
buildListeners.add(auditor);
// Parse and execute the Ant script.
executeDeploymentPhase(recipeFile, antProps, buildListeners, DeploymentPhase.STOP, null);
File deployDir = request.getAbsoluteDestinationDirectory();
DeploymentsMetadata deployMetadata = new DeploymentsMetadata(deployDir);
DeploymentPhase installPhase = (deployMetadata.isManaged()) ? DeploymentPhase.UPGRADE
: DeploymentPhase.INSTALL;
BundleAntProject project = executeDeploymentPhase(recipeFile, antProps, buildListeners, installPhase,
new PluginContainerHandoverTarget(request));
executeDeploymentPhase(recipeFile, antProps, buildListeners, DeploymentPhase.START, null);
// Send the diffs to the Server so it can store them as an entry in the deployment history.
BundleManagerProvider bundleManagerProvider = request.getBundleManagerProvider();
DeployDifferences diffs = project.getDeployDifferences();
String msg = "Added files=" + diffs.getAddedFiles().size() + "; Deleted files="
+ diffs.getDeletedFiles().size() + " (see attached details for more information)";
String fullDetails = formatDiff(diffs);
bundleManagerProvider.auditDeployment(resourceDeployment, "Deployment Differences", project.getName(),
DEPLOY_STEP, null, msg, fullDetails);
} catch (Throwable t) {
if (LOG.isDebugEnabled()) {
try {
LOG.debug(new String(StreamUtil.slurp(new FileInputStream(logFile))));
} catch (Exception ignored) {
}
}
throw new Exception("Failed to execute the bundle Ant script", t);
} finally {
if (logFileOutput != null) {
logFileOutput.close();
}
recipeFile.delete();
logFile.delete();
}
} catch (Throwable t) {
LOG.error("Failed to deploy bundle [" + request + "]", t);
result.setErrorMessage(t);
}
return result;
}
@Override
public BundlePurgeResult purgeBundle(BundlePurgeRequest request) {
BundlePurgeResult result = new BundlePurgeResult();
try {
BundleResourceDeployment deploymentToPurge = request.getLiveResourceDeployment();
File deployDir = request.getAbsoluteDestinationDirectory();
String deployDirAbsolutePath = deployDir.getAbsolutePath();
BundleManagerProvider bundleManagerProvider = request.getBundleManagerProvider();
boolean manageAllDeployDir = true;
boolean errorPurgingDeployDirContent = false;
File metadataDirectoryToPurge = null;
// If the receipe copied file(s) outside of the deployment directory (external, raw files), they will still exist.
// Let's get the metadata information that tells us if such files exist, and if so, remove them.
// Since we are iterating over the manage files anyway, let's also remove the files/subdirs under the deploy directory too.
DeploymentsMetadata metadata = new DeploymentsMetadata(deployDir);
if (metadata.isManaged()) {
metadataDirectoryToPurge = metadata.getMetadataDirectory();
//as of RHQ 4.9.0, we only only support "full" and "filesAndDirectories" destination compliance modes
//which we used to describe by boolean "manageRootDir"... Let's not use the deprecated API's but not
// change the code too much...
manageAllDeployDir = metadata.getCurrentDeploymentProperties().getDestinationCompliance() == DestinationComplianceMode.full;
int totalExternalFiles = 0;
ArrayList<String> externalDeleteSuccesses = new ArrayList<String>(0);
ArrayList<String> externalDeleteFailures = new ArrayList<String>(0);
FileHashcodeMap deployDirFileHashcodes = metadata.getCurrentDeploymentFileHashcodes();
for (String filePath : deployDirFileHashcodes.keySet()) {
File file = new File(filePath);
if (file.isAbsolute()) {
totalExternalFiles++;
if (file.exists()) {
if (file.delete()) {
externalDeleteSuccesses.add(filePath);
} else {
externalDeleteFailures.add(filePath);
}
} else {
externalDeleteSuccesses.add(filePath); // someone already deleted it, consider it removed successfully
}
} else {
// a relative path means it is inside the deploy dir (i.e. its relative to deployDir).
// note that we only remove child directories and files that are direct children of the deploy dir itself;
// we do not purge the deploy dir itself, in case we are not managing the full deploy dir.
String parentDir = file.getParent();
if (parentDir == null) {
// this file is directly in the deploy dir
file = new File(deployDir, filePath);
file.delete();
} else {
// this file is under some subdirectory under the deploy dir - purge the subdirectory completely
file = new File(deployDir, parentDir);
FileUtil.purge(file, true);
}
if (file.exists()) {
errorPurgingDeployDirContent = true;
}
}
}
if (totalExternalFiles > 0) {
if (!externalDeleteSuccesses.isEmpty()) {
StringBuilder deleteSuccessesDetails = new StringBuilder();
for (String path : externalDeleteSuccesses) {
deleteSuccessesDetails.append(path).append("\n");
}
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge", "External files were purged",
AUDIT_MESSAGE, SUCCESS, "[" + externalDeleteSuccesses.size() + "] of ["
+ totalExternalFiles
+ "] external files were purged. See attached details for the list",
deleteSuccessesDetails.toString());
}
if (!externalDeleteFailures.isEmpty()) {
StringBuilder deleteFailuresDetails = new StringBuilder();
for (String path : externalDeleteFailures) {
deleteFailuresDetails.append(path).append("\n");
}
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"External files failed to be purged", AUDIT_MESSAGE, FAILURE,
"[" + externalDeleteFailures.size() + "] of [" + totalExternalFiles
+ "] external files failed to be purged. See attached details for the list",
deleteFailuresDetails.toString());
}
}
}
// if we are managing the full deploy dir, completely purge the deployment directory.
// otherwise, just report that we deleted what we were responsible for.
if (manageAllDeployDir) {
FileUtil.purge(deployDir, true);
if (!deployDir.exists()) {
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"The destination directory has been purged", AUDIT_MESSAGE, SUCCESS, "Directory purged: "
+ deployDirAbsolutePath, null);
} else {
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"The destination directory failed to be purged", AUDIT_MESSAGE, FAILURE,
"The directory that failed to be purged: " + deployDirAbsolutePath, null);
}
} else {
if (!errorPurgingDeployDirContent) {
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"The managed bundle content was removed from the destination directory; "
+ "other unmanaged content may still remain", AUDIT_MESSAGE, SUCCESS, "Deploy Directory: "
+ deployDirAbsolutePath, null);
} else {
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"Not all managed bundle content was able to be removed from the destination directory. "
+ "That managed content along with other unmanaged content still remain", AUDIT_MESSAGE,
FAILURE, "Deploy Directory: " + deployDirAbsolutePath, null);
}
// make sure we remove the metadata directory, too - since it may still have sensitive files that were backed up
if (metadataDirectoryToPurge != null) {
FileUtil.purge(metadataDirectoryToPurge, true);
if (metadataDirectoryToPurge.exists()) {
bundleManagerProvider.auditDeployment(deploymentToPurge, "Purge",
"Failed to purge the metadata directory from the destination directory. "
+ "It may still contain backed up files from previous bundle deployments.",
AUDIT_MESSAGE, FAILURE,
"Metadata Directory: " + metadataDirectoryToPurge.getAbsolutePath(), null);
}
}
}
} catch (Throwable t) {
LOG.error("Failed to purge bundle [" + request + "]", t);
result.setErrorMessage(t);
}
return result;
}
private BundleAntProject executeDeploymentPhase(File recipeFile, Properties antProps,
List<BuildListener> buildListeners, DeploymentPhase phase, HandoverTarget handoverTarget)
throws InvalidBuildFileException {
//noinspection deprecation
AntLauncher antLauncher = new AntLauncher();
antLauncher.setHandoverTarget(handoverTarget);
antProps.setProperty(DeployPropertyNames.DEPLOY_PHASE, phase.name());
return antLauncher.executeBundleDeployFile(recipeFile, antProps, buildListeners);
}
private Properties createAntProperties(BundleDeployRequest request) {
Properties antProps = new Properties();
BundleResourceDeployment resourceDeployment = request.getResourceDeployment();
BundleDeployment bundleDeployment = resourceDeployment.getBundleDeployment();
int deploymentId = bundleDeployment.getId();
String deployDir = request.getAbsoluteDestinationDirectory().getAbsolutePath();
antProps.setProperty(DeployPropertyNames.DEPLOY_ID, Integer.toString(deploymentId));
antProps.setProperty(DeployPropertyNames.DEPLOY_DIR, deployDir);
antProps.setProperty(DeployPropertyNames.DEPLOY_NAME, bundleDeployment.getName());
antProps.setProperty(DeployPropertyNames.DEPLOY_REVERT, String.valueOf(request.isRevert()));
antProps.setProperty(DeployPropertyNames.DEPLOY_CLEAN, String.valueOf(request.isCleanDeployment()));
// add the resource tags
Set<Tag> tags = resourceDeployment.getResource().getTags();
if (tags != null) {
for (Tag tag : tags) {
String tagPropName = getTagPropertyName(tag);
if (tagPropName != null) {
antProps.setProperty(tagPropName, tag.getName());
}
}
}
// add the system info "facts"
Map<String, String> sysFacts = SystemInfoFactory.fetchTemplateEngine().getTokens();
for (Map.Entry<String, String> fact : sysFacts.entrySet()) {
antProps.setProperty(fact.getKey(), fact.getValue());
}
// add the deployment parameter properties
Configuration config = bundleDeployment.getConfiguration();
if (config != null) {
Map<String, Property> allProperties = config.getAllProperties();
for (Map.Entry<String, Property> entry : allProperties.entrySet()) {
String name = entry.getKey();
Property prop = entry.getValue();
String value;
if (prop instanceof PropertySimple) {
value = ((PropertySimple) prop).getStringValue();
} else {
// for now, just skip all property lists and maps, just assume we are getting simples
continue;
}
if (value != null) {
antProps.setProperty(name, value);
}
}
}
return antProps;
}
private String getTagPropertyName(Tag tag) {
String namespace = tag.getNamespace();
String semantic = tag.getSemantic();
if (semantic == null) {
return null; // we are ignoring tags that are not qualified with a semantic
}
if (namespace == null) {
return DeployPropertyNames.DEPLOY_TAG_PREFIX + semantic;
} else {
// note: ':' not allowed in tokens, so this is replaced with '.'
return DeployPropertyNames.DEPLOY_TAG_PREFIX + namespace + '.' + semantic;
}
}
private String formatDiff(DeployDifferences diffs) {
String indent = " ";
String nl = "\n";
StringBuilder str = new StringBuilder("DEPLOYMENT DETAILS:");
str.append(nl);
str.append("Added Files: ").append(diffs.getAddedFiles().size()).append(nl);
for (String f : diffs.getAddedFiles()) {
str.append(indent).append(f).append(nl);
}
str.append("Deleted Files: ").append(diffs.getDeletedFiles().size()).append(nl);
for (String f : diffs.getDeletedFiles()) {
str.append(indent).append(f).append(nl);
}
str.append("Changed Files: ").append(diffs.getChangedFiles().size()).append(nl);
for (String f : diffs.getChangedFiles()) {
str.append(indent).append(f).append(nl);
}
str.append("Backed Up Files: ").append(diffs.getBackedUpFiles().size()).append(nl);
for (Map.Entry<String, String> entry : diffs.getBackedUpFiles().entrySet()) {
str.append(indent).append(entry.getKey()).append(" -> ").append(entry.getValue()).append(nl);
}
str.append("Restored Files: ").append(diffs.getRestoredFiles().size()).append(nl);
for (Map.Entry<String, String> entry : diffs.getRestoredFiles().entrySet()) {
str.append(indent).append(entry.getKey()).append(" <- ").append(entry.getValue()).append(nl);
}
str.append("Ignored Files: ").append(diffs.getIgnoredFiles().size()).append(nl);
for (String f : diffs.getIgnoredFiles()) {
str.append(indent).append(f).append(nl);
}
str.append("Realized Files: ").append(diffs.getRealizedFiles().size()).append(nl);
for (String f : diffs.getRealizedFiles().keySet()) {
str.append(indent).append(f).append(nl);
}
str.append("Was Cleaned?: ").append(diffs.wasCleaned()).append(nl);
str.append("Errors: ").append(diffs.getErrors().size()).append(nl);
for (Map.Entry<String, String> entry : diffs.getErrors().entrySet()) {
str.append(indent).append(entry.getKey()).append(" : ").append(entry.getValue()).append(nl);
}
return str.toString();
}
private static class PluginContainerHandoverTarget implements HandoverTarget {
final BundleDeployRequest request;
PluginContainerHandoverTarget(BundleDeployRequest request) {
this.request = request;
}
@Override
public boolean handoverContent(HandoverInfo handoverInfo) {
BundleResourceDeployment resourceDeployment = request.getResourceDeployment();
BundleManagerProvider bundleManagerProvider = request.getBundleManagerProvider();
BundleHandoverContext.Builder contextBuilder = new BundleHandoverContext.Builder();
contextBuilder.setRevert(handoverInfo.isRevert());
BundleHandoverRequest.Builder handoverRequestBuilder = new BundleHandoverRequest.Builder();
handoverRequestBuilder.setContent(handoverInfo.getContent()) //
.setFilename(handoverInfo.getFilename()) //
.setAction(handoverInfo.getAction()).setParams(handoverInfo.getParams())//
.setContext(contextBuilder.create());
BundleHandoverRequest bundleHandoverRequest = handoverRequestBuilder.createBundleHandoverRequest();
BundleHandoverResponse handoverResponse = bundleManagerProvider.handoverContent(
resourceDeployment.getResource(), bundleHandoverRequest);
boolean success = handoverResponse.isSuccess();
try {
StringWriter attachmentStringWriter = new StringWriter();
PrintWriter attachmentPrintWriter = new PrintWriter(attachmentStringWriter, true);
attachmentPrintWriter.println(bundleHandoverRequest);
if (success) {
bundleManagerProvider.auditDeployment(resourceDeployment, "Handover",
"Successful content handover to bundle target resource", AUDIT_MESSAGE, SUCCESS,
handoverResponse.getMessage(), attachmentStringWriter.toString());
} else {
String handoverFailure = getHandoverFailure(handoverResponse);
Throwable handoverResponseThrowable = handoverResponse.getThrowable();
if (handoverResponseThrowable != null) {
attachmentPrintWriter.println();
attachmentPrintWriter.println(ThrowableUtil.getAllMessages(handoverResponseThrowable));
}
bundleManagerProvider.auditDeployment(resourceDeployment, "Handover", handoverFailure,
AUDIT_MESSAGE, FAILURE, handoverResponse.getMessage(), attachmentStringWriter.toString());
}
} catch (Exception e) {
LOG.warn("Unexpected failure while auditing deployment", e);
}
return success;
}
private String getHandoverFailure(BundleHandoverResponse handoverReport) {
String handoverFailure;
switch (handoverReport.getFailureType()) {
case INVALID_ACTION:
handoverFailure = "Invalid handover action";
break;
case MISSING_PARAMETER:
handoverFailure = "Missing required handover parameter";
break;
case INVALID_PARAMETER:
handoverFailure = "Invalid handover parameter";
break;
case PLUGIN_CONTAINER:
handoverFailure = "Handover invocation failed in the plugin container";
break;
case EXECUTION:
handoverFailure = "Handover failed during execution";
break;
default:
handoverFailure = "Unknown handover failure";
break;
}
return handoverFailure;
}
}
}