/*
* RHQ Management Platform
* Copyright (C) 2005-2009 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.plugins.mobicents.servlet.sip.jboss5;
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.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.deployers.spi.management.deploy.DeploymentManager;
import org.jboss.deployers.spi.management.deploy.DeploymentProgress;
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.domain.resource.ResourceType;
import org.rhq.core.pluginapi.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;
import org.rhq.core.pluginapi.content.version.PackageVersions;
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.plugins.jbossas5.factory.ProfileServiceFactory;
import org.rhq.plugins.mobicents.servlet.sip.jboss5.util.DeploymentUtils;
/**
* @author Ian Springer
*/
public class StandaloneManagedDeploymentComponent extends AbstractManagedDeploymentComponent
implements MeasurementFacet, ContentFacet, DeleteResourceFacet
{
public static final String RESOURCE_TYPE_EAR = "Enterprise Application (EAR)";
public static final String RESOURCE_TYPE_WAR = "Web Application (WAR)";
public static final String RESOURCE_TYPE_CONVERGED_WAR = "Converged SIP/Web Application (WAR)";
public static final String RESOURCE_TYPE_RAR = "Resource Adaptor (RAR)";
private static final String CUSTOM_PATH_TRAIT = "custom.path";
private static final String CUSTOM_EXPLODED_TRAIT = "custom.exploded";
/**
* 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 String BACKUP_FILE_EXTENSION = ".rej";
private final Log log = LogFactory.getLog(this.getClass());
private PackageVersions versions;
// ------------ MeasurementFacet Implementation ------------
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
for (MeasurementScheduleRequest request : requests) {
String metricName = request.getName();
if (metricName.equals(CUSTOM_PATH_TRAIT)) {
MeasurementDataTrait trait = new MeasurementDataTrait(request, this.deploymentFile.getPath());
report.addData(trait);
}
else if (metricName.equals(CUSTOM_EXPLODED_TRAIT)) {
boolean exploded = this.deploymentFile.isDirectory();
MeasurementDataTrait trait = new MeasurementDataTrait(request, (exploded) ? "yes" : "no");
report.addData(trait);
}
}
}
// ------------ ContentFacet implementation -------------
public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {
File packageFile = new File(packageDetails.getName());
File fileToSend;
try {
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);
}
}
public Set<ResourcePackageDetails> discoverDeployedPackages(PackageType packageType) {
if (!this.deploymentFile.exists())
throw new IllegalStateException("Deployment file '" + this.deploymentFile + "' for " +
getResourceDescription() + " does not exist.");
String fileName = this.deploymentFile.getName();
PackageVersions packageVersions = loadPackageVersions();
String version = packageVersions.getVersion(fileName);
if (version == null) {
// This is either the first time we've discovered this EAR/WAR, or someone purged the PC's data dir.
version = "1.0";
packageVersions.putVersion(fileName, version);
packageVersions.saveToDisk();
}
// 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(this.deploymentFile.getPath());
if (!this.deploymentFile.isDirectory())
packageDetails.setFileSize(this.deploymentFile.length());
packageDetails.setFileCreatedDate(null); // TODO: get created date via SIGAR
Set<ResourcePackageDetails> packages = new HashSet<ResourcePackageDetails>();
packages.add(packageDetails);
return packages;
}
public RemovePackagesResponse removePackages(Set<ResourcePackageDetails> packages) {
throw new UnsupportedOperationException("Cannot remove the package backing an EAR/WAR resource.");
}
public List<DeployPackageStep> generateInstallationSteps(ResourcePackageDetails packageDetails) {
// Intentional - there are no steps involved in installing an EAR or WAR.
return null;
}
public DeployPackagesResponse deployPackages(Set<ResourcePackageDetails> packages, ContentServices contentServices) {
// 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 an EAR/WAR file contained multiple packages: " + packages);
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
response.setOverallRequestErrorMessage("When updating an EAR/WAR, only one EAR/WAR can be updated at a time.");
return response;
}
ResourcePackageDetails packageDetails = packages.iterator().next();
log.debug("Updating EAR/WAR file '" + this.deploymentFile + "' using [" + packageDetails + "]...");
// Find location of existing application.
if (!this.deploymentFile.exists()) {
return failApplicationDeployment("Could not find application to update at location: " + this.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);
}
log.debug("Wrote new EAR/WAR bits to temporary file '" + tempFile + "'.");
boolean deployExploded = this.deploymentFile.isDirectory();
// Backup the original app file/dir to <filename>.rej.
File backupOfOriginalFile = new File(this.deploymentFile.getPath() + BACKUP_FILE_EXTENSION);
log.debug("Backing up existing EAR/WAR '" + this.deploymentFile + "' to '" + backupOfOriginalFile + "'...");
try
{
if (backupOfOriginalFile.exists())
FileUtils.forceDelete(backupOfOriginalFile);
if (this.deploymentFile.isDirectory())
FileUtils.copyDirectory(this.deploymentFile, backupOfOriginalFile, true);
else
FileUtils.copyFile(this.deploymentFile, backupOfOriginalFile, true);
}
catch (Exception e)
{
throw new RuntimeException("Failed to backup existing EAR/WAR '" + this.deploymentFile + "' to '"
+ backupOfOriginalFile + "'.");
}
// Now stop the original app.
try
{
DeploymentManager deploymentManager = ProfileServiceFactory.getDeploymentManager();
DeploymentProgress progress = deploymentManager.stop(this.deploymentName);
DeploymentUtils.run(progress);
}
catch (Exception e)
{
throw new RuntimeException("Failed to stop deployment [" + this.deploymentName + "].", e);
}
// And then remove it (this will delete the physical file/dir from the deploy dir).
try
{
DeploymentManager deploymentManager = ProfileServiceFactory.getDeploymentManager();
DeploymentProgress progress = deploymentManager.remove(this.deploymentName);
DeploymentUtils.run(progress);
}
catch (Exception e)
{
throw new RuntimeException("Failed to remove deployment [" + this.deploymentName + "].", e);
}
// Deploy away!
log.debug("Deploying '" + tempFile + "'...");
File deployDir = this.deploymentFile.getParentFile();
try {
DeploymentUtils.deployArchive(tempFile, deployDir, 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 {
// Delete the new app, which failed to deploy.
FileUtils.forceDelete(this.deploymentFile);
// Need to redeploy the original file - this generally should succeed.
DeploymentUtils.deployArchive(backupOfOriginalFile, deployDir, deployExploded);
errorMessage += " ***** ROLLED BACK TO ORIGINAL APPLICATION FILE. *****";
}
catch (Exception e1) {
log.debug("Rollback failed!", e1);
errorMessage += " ***** FAILED TO ROLLBACK TO ORIGINAL APPLICATION FILE. *****: "
+ ThrowableUtil.getAllMessages(e1);
}
log.info("Failed to update EAR/WAR file '" + this.deploymentFile + "' using [" + packageDetails + "]." );
return failApplicationDeployment(errorMessage, packageDetails);
}
// Deploy was successful!
deleteBackupOfOriginalFile(backupOfOriginalFile);
persistApplicationVersion(packageDetails, this.deploymentFile);
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.SUCCESS);
DeployIndividualPackageResponse packageResponse =
new DeployIndividualPackageResponse(packageDetails.getKey(), ContentResponseResult.SUCCESS);
response.addPackageResponse(packageResponse);
log.debug("Updated EAR/WAR file '" + this.deploymentFile + "' successfully - returning response [" + response
+ "]...");
return response;
}
// ------------ DeleteResourceFacet implementation -------------
public void deleteResource() throws Exception {
DeploymentManager deploymentManager = ProfileServiceFactory.getDeploymentManager();
log.debug("Stopping deployment [" + this.deploymentName + "]...");
DeploymentProgress progress = deploymentManager.stop(deploymentName);
DeploymentUtils.run(progress);
log.debug("Removing deployment [" + this.deploymentName + "]...");
progress = deploymentManager.remove(deploymentName);
DeploymentUtils.run(progress);
}
// ------------ AbstractManagedComponent implementation -------------
@Override
protected Log getLog()
{
return this.log;
}
/**
* 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 persistApplicationVersion(ResourcePackageDetails packageDetails, File appFile) {
String packageName = appFile.getName();
log.debug("Persisting application version '" + packageDetails.getVersion() + "' for package '" + packageName
+ "'");
PackageVersions versions = loadPackageVersions();
versions.putVersion(packageName, packageDetails.getVersion());
}
private void deleteBackupOfOriginalFile(File backupOfOriginalFile) {
log.debug("Deleting backup of original file '" + backupOfOriginalFile + "'...");
try {
FileUtils.forceDelete(backupOfOriginalFile);
}
catch (Exception e) {
// not critical.
log.warn("Failed to delete backup of original file: " + backupOfOriginalFile);
}
}
private File writeNewAppBitsToTempFile(ContentServices contentServices, ResourcePackageDetails packageDetails
) throws Exception {
File tempDir = getResourceContext().getTemporaryDirectory();
File tempFile = new File(tempDir, this.deploymentFile.getName());
OutputStream tempOutputStream = null;
try {
tempOutputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
long bytesWritten = contentServices.downloadPackageBits(getResourceContext().getContentContext(), packageDetails.getKey(),
tempOutputStream, true);
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;
}
/**
* Returns an instantiated and loaded versions store access point.
*
* @return will not be <code>null</code>
*/
private PackageVersions loadPackageVersions() {
if (this.versions == null) {
ResourceType resourceType = getResourceContext().getResourceType();
String pluginName = resourceType.getPlugin();
File dataDirectoryFile = getResourceContext().getDataDirectory();
dataDirectoryFile.mkdirs();
String dataDirectory = dataDirectoryFile.getAbsolutePath();
log.trace("Creating application versions store with plugin name [" + pluginName +
"] and data directory [" + dataDirectory + "]");
this.versions = new PackageVersions(pluginName, dataDirectory);
this.versions.loadFromDisk();
}
return this.versions;
}
}