/*
* 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, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* 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 and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser 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.jbossas5;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.deployers.spi.management.ManagementView;
import org.jboss.deployers.spi.management.deploy.DeploymentManager;
import org.jboss.deployers.spi.management.deploy.DeploymentProgress;
import org.jboss.deployers.spi.management.deploy.DeploymentStatus;
import org.jboss.profileservice.spi.ProfileKey;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.PackageType;
import org.rhq.core.domain.content.transfer.ContentResponseResult;
import org.rhq.core.domain.content.transfer.DeployIndividualPackageResponse;
import org.rhq.core.domain.content.transfer.DeployPackageStep;
import org.rhq.core.domain.content.transfer.DeployPackagesResponse;
import org.rhq.core.domain.content.transfer.RemovePackagesResponse;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
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.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;
import org.rhq.core.pluginapi.content.FileContentDelegate;
import org.rhq.core.pluginapi.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.util.ZipUtil;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.ContentFileInfo;
import org.rhq.core.util.file.JarContentFileInfo;
import org.rhq.plugins.jbossas5.connection.ProfileServiceConnection;
import org.rhq.plugins.jbossas5.util.DeploymentUtils;
/**
* A resource component for managing a standalone/top-level Profile Service managed deployment.
*
* @author Ian Springer
*/
public class StandaloneManagedDeploymentComponent extends AbstractManagedDeploymentComponent implements
MeasurementFacet, ContentFacet, DeleteResourceFacet {
private static final Log LOG = LogFactory.getLog(StandaloneManagedDeploymentComponent.class);
private static final String CUSTOM_PATH_TRAIT = "custom.path";
private static final String CUSTOM_EXPLODED_TRAIT = "custom.exploded";
/**
* @deprecated as of 4.13. No longer used, at least since 4.12.
*/
@Deprecated
public static final String RHQ_SHA256 = "RHQ-Sha256";
/**
* Name of the backing package type that will be used when discovering packages. This corresponds to the name of the
* package type defined in the plugin descriptor. For simplicity, the package type for both EARs and WARs is simply
* called "file". This is still unique within the context of the parent resource type and lets this class use the
* same package type name in both cases.
*/
private static final String PKG_TYPE_FILE = "file";
/**
* Architecture string used in describing discovered packages.
*/
private static final String ARCHITECTURE = "noarch";
private static final ProfileKey FARM_PROFILE_KEY = new ProfileKey("farm");
private static final ProfileKey APPLICATIONS_PROFILE_KEY = new ProfileKey("applications");
// ------------ MeasurementFacet Implementation ------------
@Override
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) throws Exception {
Set<MeasurementScheduleRequest> remainingRequests = new HashSet<MeasurementScheduleRequest>();
for (MeasurementScheduleRequest request : requests) {
String metricName = request.getName();
if (metricName.equals(CUSTOM_PATH_TRAIT)) {
MeasurementDataTrait trait = new MeasurementDataTrait(request, deploymentFile.getPath());
report.addData(trait);
} else if (metricName.equals(CUSTOM_EXPLODED_TRAIT)) {
boolean exploded = deploymentFile.isDirectory();
MeasurementDataTrait trait = new MeasurementDataTrait(request, (exploded) ? "yes" : "no");
report.addData(trait);
} else {
remainingRequests.add(request);
}
}
super.getValues(report, remainingRequests);
}
// ------------ ContentFacet implementation -------------
@Override
public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {
File packageFile = new File(packageDetails.getName());
File fileToSend;
try {
/*
* TODO: This all seems very broken. We are first setting packageFile
* to the key used for packageDetails and not the fileName of packageDetails.
* Additionally, PackageDetails.fileName contains the file name without its
* path making its use invalid here.
*/
// If the file isn't real then lets fall-back to this ManagedDeploymentComponent's file name and hope its valid
if (!packageFile.exists() && deploymentFile != null) {
packageFile = deploymentFile;
}
if (packageFile.isDirectory()) {
fileToSend = File.createTempFile("rhq", ".zip");
ZipUtil.zipFileOrDirectory(packageFile, fileToSend);
} else
fileToSend = packageFile;
return new BufferedInputStream(new FileInputStream(fileToSend));
} catch (IOException e) {
throw new RuntimeException("Failed to retrieve package bits for " + packageDetails, e);
}
}
@Override
public Set<ResourcePackageDetails> discoverDeployedPackages(PackageType packageType) {
if (!deploymentFile.exists())
throw new IllegalStateException("Deployment file '" + deploymentFile + "' for " + getResourceDescription()
+ " does not exist.");
String fileName = deploymentFile.getName();
String sha256 = getSHA256(deploymentFile);
String version = getVersion(sha256);
String displayVersion = getDisplayVersion(deploymentFile);
// Package name is the deployment's file name (e.g. foo.ear).
PackageDetailsKey key = new PackageDetailsKey(fileName, version, PKG_TYPE_FILE, ARCHITECTURE);
ResourcePackageDetails packageDetails = new ResourcePackageDetails(key);
packageDetails.setFileName(fileName);
packageDetails.setLocation(deploymentFile.getPath());
if (!deploymentFile.isDirectory())
packageDetails.setFileSize(deploymentFile.length());
packageDetails.setFileCreatedDate(null); // TODO: get created date via SIGAR
packageDetails.setSHA256(sha256);
packageDetails.setInstallationTimestamp(Long.valueOf(System.currentTimeMillis()));
packageDetails.setDisplayVersion(displayVersion);
Set<ResourcePackageDetails> packages = new HashSet<ResourcePackageDetails>();
packages.add(packageDetails);
return packages;
}
/**
* Retrieve SHA256 for a deployed app.
*
* @param file application file
* @return SHA256 of the content
*/
private String getSHA256(File file) {
String sha256 = null;
try {
FileContentDelegate fileContentDelegate = new FileContentDelegate();
sha256 = fileContentDelegate.retrieveDeploymentSHA(file, getResourceContext().getResourceDataDirectory());
} catch (Exception iex) {
if (LOG.isDebugEnabled()) {
LOG.debug("Problem calculating digest of package [" + file.getPath() + "]." + iex.getMessage());
}
}
return sha256;
}
private String getVersion(String sha256) {
return "[sha256=" + sha256 + "]";
}
/**
* Retrieve the display version for the component. The display version should be stored
* in the manifest of the application (implementation and/or specification version).
* It will attempt to retrieve the version for both archived or exploded deployments.
*
* @param file component file
* @return
*/
private String getDisplayVersion(File file) {
//JarContentFileInfo extracts the version from archived and exploded deployments
ContentFileInfo contentFileInfo = new JarContentFileInfo(file);
return contentFileInfo.getVersion(null);
}
@Override
public RemovePackagesResponse removePackages(Set<ResourcePackageDetails> packages) {
throw new UnsupportedOperationException("Cannot remove the package backing an EAR/WAR resource.");
}
@Override
public List<DeployPackageStep> generateInstallationSteps(ResourcePackageDetails packageDetails) {
// Intentional - there are no steps involved in installing an EAR or WAR.
return null;
}
@Override
public DeployPackagesResponse deployPackages(Set<ResourcePackageDetails> packages, ContentServices contentServices) {
String resourceTypeName = getResourceContext().getResourceType().getName();
// You can only update the one application file referenced by this resource, so punch out if multiple are
// specified.
if (packages.size() != 1) {
LOG.warn("Request to update " + resourceTypeName + " file contained multiple packages: " + packages);
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
response.setOverallRequestErrorMessage("Only one " + resourceTypeName + " can be updated at a time.");
return response;
}
ResourcePackageDetails packageDetails = packages.iterator().next();
if (LOG.isDebugEnabled()) {
LOG.debug("Updating EAR/WAR file '" + deploymentFile + "' using [" + packageDetails + "]...");
}
// Find location of existing application.
if (!deploymentFile.exists()) {
return failApplicationDeployment("Could not find application to update at location: " + deploymentFile,
packageDetails);
}
LOG.debug("Writing new EAR/WAR bits to temporary file...");
File tempFile;
try {
tempFile = writeNewAppBitsToTempFile(contentServices, packageDetails);
} catch (Exception e) {
return failApplicationDeployment("Error writing new application bits to temporary file - cause: " + e,
packageDetails);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Wrote new EAR/WAR bits to temporary file '" + tempFile + "'.");
}
boolean deployExploded = deploymentFile.isDirectory();
// Backup the original app file/dir.
File tempDir = getResourceContext().getTemporaryDirectory();
File backupDir = new File(tempDir, "deployBackup" + UUID.randomUUID().getLeastSignificantBits());
File backupOfOriginalFile = new File(backupDir, deploymentFile.getName());
if (LOG.isDebugEnabled()) {
LOG.debug("Backing up existing EAR/WAR '" + deploymentFile + "' to '" + backupOfOriginalFile + "'...");
}
try {
if (backupOfOriginalFile.exists()) {
FileUtils.forceDelete(backupOfOriginalFile);
}
if (deploymentFile.isDirectory()) {
FileUtils.copyDirectory(deploymentFile, backupOfOriginalFile, true);
} else {
FileUtils.copyFile(deploymentFile, backupOfOriginalFile, true);
}
} catch (Exception e) {
throw new RuntimeException("Failed to backup existing " + resourceTypeName + "'" + deploymentFile
+ "' to '" + backupOfOriginalFile + "'.");
}
ProfileServiceConnection connection = getConnection();
if (connection == null) {
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
response.setOverallRequestErrorMessage("No profile service connection available");
return response;
}
DeploymentManager deploymentManager = connection.getDeploymentManager();
// as crazy as it might sound, there is apparently no way for you to ask the profile service
// if a deployment was deployed to the farm profile. Thus, we must resort to a poor man's solution:
// if the deployment name has the "farm/" directory in it, assume it needs to be deployed to the farm
boolean deployFarmed = getDeploymentKey().contains("/farm/");
if (deployFarmed) {
Collection<ProfileKey> profileKeys = deploymentManager.getProfiles();
boolean farmSupported = false;
for (ProfileKey profileKey : profileKeys) {
if (profileKey.getName().equals(FARM_PROFILE_KEY.getName())) {
farmSupported = true;
break;
}
}
if (!farmSupported) {
throw new IllegalStateException("This application server instance is not a node in a cluster, "
+ "so it does not support farmed deployments. Supported deployment profiles are " + profileKeys
+ ".");
}
if (deployExploded) {
throw new IllegalArgumentException(
"Deploying farmed applications in exploded form is not supported by the Profile Service.");
}
try {
deploymentManager.loadProfile(FARM_PROFILE_KEY);
} catch (Exception e) {
LOG.info("Failed to switch to farm profile - could not update " + resourceTypeName + " file '"
+ deploymentFile + "' using [" + packageDetails + "].");
String errorMessage = ThrowableUtil.getAllMessages(e);
return failApplicationDeployment(errorMessage, packageDetails);
}
}
String deploymentName = getDeploymentName();
if (deploymentName == null) {
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
response.setOverallRequestErrorMessage("Did not find deployment with key [" + getDeploymentKey() + "]");
return response;
}
// Now stop the original app.
try {
DeploymentProgress progress = deploymentManager.stop(deploymentName);
DeploymentUtils.run(progress);
} catch (Exception e) {
throw new RuntimeException("Failed to stop deployment [" + deploymentName + "].", e);
}
// And then remove it (this will delete the physical file/dir from the deploy dir).
try {
DeploymentProgress progress = deploymentManager.remove(deploymentName);
DeploymentUtils.run(progress);
} catch (Exception e) {
throw new RuntimeException("Failed to remove deployment [" + deploymentName + "].", e);
}
// Deploy away!
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Deploying '" + tempFile + "'...");
}
DeploymentUtils.deployArchive(deploymentManager, tempFile, deployExploded);
} catch (Exception e) {
// Deploy failed - rollback to the original app file...
LOG.debug("Redeploy failed - rolling back to original archive...", e);
String errorMessage = ThrowableUtil.getAllMessages(e);
try {
// Try to delete the new app file, which failed to deploy, if it still exists.
if (deploymentFile.exists()) {
try {
FileUtils.forceDelete(deploymentFile);
} catch (IOException e1) {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed to delete application file '" + deploymentFile
+ "' that failed to deploy.", e1);
}
}
}
// Now redeploy the original file - this generally should succeed.
DeploymentUtils.deployArchive(deploymentManager, backupOfOriginalFile, deployExploded);
errorMessage += " ***** ROLLED BACK TO ORIGINAL APPLICATION FILE. *****";
// If the redeployment of the original backup succeeded then cleanup the backup from disk
deleteTemporaryFile(backupDir);
// If the redeployment fails the original backup is preserved on disk until agent restart
} catch (Exception e1) {
LOG.debug("Rollback failed!", e1);
errorMessage += " ***** FAILED TO ROLLBACK TO ORIGINAL APPLICATION FILE. *****: "
+ ThrowableUtil.getAllMessages(e1);
}
//since the deployment failed remove the temp application downloaded for deployment
deleteTemporaryFile(tempFile);
LOG.info("Failed to update " + resourceTypeName + " file '" + deploymentFile + "' using [" + packageDetails
+ "].");
return failApplicationDeployment(errorMessage, packageDetails);
} finally {
// Make sure to switch back to the 'applications' profile if we switched to the 'farm' profile above.
if (deployFarmed) {
try {
deploymentManager.loadProfile(APPLICATIONS_PROFILE_KEY);
} catch (Exception e) {
LOG.debug("Failed to switch back to applications profile from farm profile", e);
}
}
}
// Store SHA256 in the agent file if deployment was exploded
if (deploymentFile.isDirectory()) {
FileContentDelegate fileContentDelegate = new FileContentDelegate();
fileContentDelegate.saveDeploymentSHA(tempFile, deploymentFile, getResourceContext()
.getResourceDataDirectory());
}
// Remove temporary files created by this deployment.
deleteTemporaryFile(backupDir);
deleteTemporaryFile(tempFile.getParentFile());
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.SUCCESS);
DeployIndividualPackageResponse packageResponse = new DeployIndividualPackageResponse(packageDetails.getKey(),
ContentResponseResult.SUCCESS);
response.addPackageResponse(packageResponse);
if (LOG.isDebugEnabled()) {
LOG.debug("Updated " + resourceTypeName + " file '" + deploymentFile
+ "' successfully - returning response [" + response + "]...");
}
return response;
}
// ------------ DeleteResourceFacet implementation -------------
@Override
public void deleteResource() throws Exception {
if (LOG.isDebugEnabled()) {
LOG.debug("Deleting " + getResourceDescription() + "...");
}
ProfileServiceConnection connection = getConnection();
if (connection == null) {
throw new Exception("No profile service connection available");
}
DeploymentManager deploymentManager = connection.getDeploymentManager();
try {
getManagedDeployment();
} catch (Exception e) {
// The deployment no longer exists, so there's nothing for us to do. Someone most likely undeployed it
// outside of Jopr or EmbJopr, e.g. via the jmx-console or by deleting the app file from the deploy dir.
LOG.warn("Cannot delete the deployment [" + getDeploymentKey() + "], since it no longer exists");
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Stopping deployment [" + getDeploymentKey() + "]...");
}
String deploymentName = getDeploymentName();
if (deploymentName == null) {
throw new IllegalStateException("Deployment " + getDeploymentKey() + " has vanished");
}
DeploymentProgress progress = deploymentManager.stop(deploymentName);
DeploymentStatus stopStatus = DeploymentUtils.run(progress);
if (stopStatus.isFailed()) {
LOG.error("Failed to stop deployment '" + deploymentName + "'.", stopStatus.getFailure());
throw new Exception("Failed to stop deployment '" + deploymentName + "' - cause: "
+ stopStatus.getFailure());
}
if (LOG.isDebugEnabled()) {
LOG.debug("Removing deployment [" + deploymentName + "]...");
}
progress = deploymentManager.remove(deploymentName);
DeploymentStatus removeStatus = DeploymentUtils.run(progress);
if (removeStatus.isFailed()) {
LOG.error("Failed to remove deployment '" + deploymentName + "'.", removeStatus.getFailure());
throw new Exception("Failed to remove deployment '" + deploymentName + "' - cause: "
+ removeStatus.getFailure());
}
ManagementView managementView = connection.getManagementView();
managementView.load();
}
/**
* Creates the necessary transfer objects to report a failed application deployment (update).
*
* @param errorMessage reason the deploy failed
* @param packageDetails describes the update being made
* @return response populated to reflect a failure
*/
private DeployPackagesResponse failApplicationDeployment(String errorMessage, ResourcePackageDetails packageDetails) {
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
DeployIndividualPackageResponse packageResponse = new DeployIndividualPackageResponse(packageDetails.getKey(),
ContentResponseResult.FAILURE);
packageResponse.setErrorMessage(errorMessage);
response.addPackageResponse(packageResponse);
return response;
}
private void deleteTemporaryFile(File temporaryFile) {
if (LOG.isDebugEnabled()) {
LOG.debug("Deleting temporary file '" + temporaryFile + "'...");
}
try {
FileUtils.forceDelete(temporaryFile);
} catch (Exception e) {
// not critical.
LOG.warn("Failed to temporary file: " + temporaryFile);
}
}
private File writeNewAppBitsToTempFile(ContentServices contentServices, ResourcePackageDetails packageDetails)
throws Exception {
File tempDir = new File(getResourceContext().getTemporaryDirectory(), "deploy"
+ UUID.randomUUID().getLeastSignificantBits());
tempDir.mkdirs();
File tempFile = new File(tempDir, deploymentFile.getName());
OutputStream tempOutputStream = null;
try {
tempOutputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
long bytesWritten = contentServices.downloadPackageBits(getResourceContext().getContentContext(),
packageDetails.getKey(), tempOutputStream, true);
if (LOG.isDebugEnabled()) {
LOG.debug("Wrote " + bytesWritten + " bytes to '" + tempFile + "'.");
}
} catch (IOException e) {
LOG.error("Error writing updated application bits to temporary location: " + tempFile, e);
throw e;
} finally {
if (tempOutputStream != null) {
try {
tempOutputStream.close();
} catch (IOException e) {
LOG.error("Error closing temporary output stream", e);
}
}
}
if (!tempFile.exists()) {
LOG.error("Temporary file for application update not written to: " + tempFile);
throw new Exception();
}
return tempFile;
}
}