/*
* RHQ Management Platform
* Copyright (C) 2005-2008 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.jbossas;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
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 java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.configuration.Configuration;
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.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.pluginapi.util.FileUtils;
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.jbossas.helper.MainDeployer;
import org.rhq.plugins.jbossas.util.FileContentDelegate;
import org.rhq.plugins.jmx.MBeanResourceComponent;
/**
* A resource component for managing an application (e.g. EAR or WAR) deployed to a JBossAS server.
*
* @author Ian Springer
*/
public class ApplicationComponent extends MBeanResourceComponent<JBossASServerComponent<?>> implements ContentFacet,
DeleteResourceFacet, OperationFacet {
private static final String BACKUP_FILE_SUFFIX = ".rej";
/**
* 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 FILENAME_PLUGIN_CONFIG_PROP = "filename";
private static final String APPLICATION_PATH_TRAIT = "Application.path";
private static final String APPLICATION_EXPLODED_TRAIT = "Application.exploded";
private final Log log = LogFactory.getLog(this.getClass());
// ContentFacet Implementation --------------------------------------------
public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {
Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
String fullFileName = pluginConfiguration.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);
File packageFile = new File(fullFileName);
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 type) {
Set<ResourcePackageDetails> packages = new HashSet<ResourcePackageDetails>();
Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
String fullFileName = pluginConfiguration.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);
if (fullFileName == null) {
throw new IllegalStateException(
"Plugin configuration does not contain the full file name of the EAR/WAR file.");
}
// If the parent EAR/WAR resource was found, this file should exist
File file = new File(fullFileName);
if (file.exists()) {
// Package name and file name of the application are the same
String fileName = new File(fullFileName).getName();
String sha256 = getSHA256(file);
String version = getVersion(sha256);
String displayVersion = getDisplayVersion(file);
PackageDetailsKey key = new PackageDetailsKey(fileName, version, PKG_TYPE_FILE, ARCHITECTURE);
ResourcePackageDetails details = new ResourcePackageDetails(key);
details.setFileName(fileName);
details.setLocation(file.getPath());
if (!file.isDirectory())
details.setFileSize(file.length());
details.setFileCreatedDate(file.lastModified()); // TODO: get created date via SIGAR
details.setSHA256(sha256);
details.setInstallationTimestamp(Long.valueOf(System.currentTimeMillis()));
details.setDisplayVersion(displayVersion);
packages.add(details);
}
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.");
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.FAILURE);
response
.setOverallRequestErrorMessage("When deploying an EAR/WAR, only one EAR/WAR can be updated at a time.");
return response;
}
ResourcePackageDetails packageDetails = packages.iterator().next();
// Find location of existing application
Configuration pluginConfig = getResourceContext().getPluginConfiguration();
File appFile = new File(pluginConfig.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue());
if (!appFile.exists()) {
return failApplicationDeployment("Could not find application to update at location: " + appFile,
packageDetails);
}
File tempFile;
try {
tempFile = writeNewAppBitsToTempFile(appFile, contentServices, packageDetails);
} catch (Exception e) {
return failApplicationDeployment("Error writing new application bits to temporary file - cause: " + e,
packageDetails);
}
//Before the backup find out if the current deployment is exploded or not.
//Do not do this after the backup because the original files are no longer
//on disk and thus .isDirectory() always returns false.
boolean isExploded = appFile.isDirectory();
// Backup the existing app file/dir to <filename>.rej.
File backupOfOriginalFile = new File(appFile.getPath() + BACKUP_FILE_SUFFIX);
appFile.renameTo(backupOfOriginalFile);
// Write the new bits for the application
moveTempFileToDeployLocation(tempFile, appFile, isExploded);
// The file has been written successfully to the deploy dir. Now try to actually deploy it.
MainDeployer mainDeployer = getParentResourceComponent().getMainDeployer();
try {
mainDeployer.redeploy(appFile);
// Deploy was successful, delete the backup
try {
FileUtils.purge(backupOfOriginalFile, true);
} catch (Exception e) {
log.warn("Failed to delete backup of original file: " + backupOfOriginalFile);
}
} catch (Exception e) {
// Deploy failed - rollback to the original app file...
String errorMessage = ThrowableUtil.getAllMessages(e);
try {
FileUtils.purge(appFile, true);
backupOfOriginalFile.renameTo(appFile);
// Need to redeploy the original file - this generally should succeed.
mainDeployer.redeploy(appFile);
errorMessage += " ***** ROLLED BACK TO ORIGINAL APPLICATION FILE. *****";
} catch (Exception e1) {
errorMessage += " ***** FAILED TO ROLLBACK TO ORIGINAL APPLICATION FILE. *****: "
+ ThrowableUtil.getAllMessages(e1);
}
return failApplicationDeployment(errorMessage, packageDetails);
}
DeployPackagesResponse response = new DeployPackagesResponse(ContentResponseResult.SUCCESS);
DeployIndividualPackageResponse packageResponse = new DeployIndividualPackageResponse(packageDetails.getKey(),
ContentResponseResult.SUCCESS);
response.addPackageResponse(packageResponse);
return response;
}
// DeleteResourceFacet Implementation --------------------------------------------
public void deleteResource() throws Exception {
Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
String fullFileName = pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();
File file = new File(fullFileName);
if (file.exists()) {
try {
getParentResourceComponent().undeployFile(file);
} catch (Exception e) {
log.error("Failed to undeploy file [" + file + "].", e);
throw e;
} finally {
try {
FileUtils.purge(file, true);
} catch (IOException e) {
log.error("Failed to delete file [" + file + "].", e);
// if the undeploy also failed that exception will be lost
// and this one will be seen by the caller instead.
// arguably both these conditions indicate failure, since
// not being able to delete the file will mean that it will
// likely get picked up again by the deployment scanner
throw e;
}
}
} else {
log.info("deleteResource: File [" + fullFileName + "] was not found - ignoring.");
}
}
// MeasurementFacet Implementation --------------------------------------------
@Override
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
if (!requests.isEmpty()) {
Configuration pluginConfig = getResourceContext().getPluginConfiguration();
String path = pluginConfig.getSimpleValue(FILENAME_PLUGIN_CONFIG_PROP, null);
for (MeasurementScheduleRequest request : requests) {
String metricName = request.getName();
if (metricName.equals(APPLICATION_PATH_TRAIT)) {
MeasurementDataTrait trait = new MeasurementDataTrait(request, path);
report.addData(trait);
} else if (metricName.equals(APPLICATION_EXPLODED_TRAIT)) {
boolean exploded = new File(path).isDirectory();
MeasurementDataTrait trait = new MeasurementDataTrait(request, (exploded) ? "yes" : "no");
report.addData(trait);
}
}
}
}
// OperationFacet Implementation ------------------------------------------------
public OperationResult invokeOperation(String name, Configuration parameters) throws InterruptedException,
Exception {
if ("revert".equals(name)) {
try {
revertFromBackupFile();
return new OperationResult("Successfully reverted from backup");
} catch (Exception e) {
throw new RuntimeException("Error reverting from Backup: " + e.getMessage());
}
} else {
return super.invokeOperation(name, parameters);
}
}
// Public --------------------------------------------
/**
* Returns the name of the application.
*
* @return application name
*/
public String getApplicationName() {
String resourceKey = getResourceContext().getResourceKey();
return resourceKey.substring(resourceKey.lastIndexOf('=') + 1);
}
/**
* Returns the file name of this application.
*
* @return full directory and file name of the application
*/
public String getFileName() {
Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
return pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();
}
public JBossASServerComponent<?> getParentResourceComponent() {
return getResourceContext().getParentResourceComponent();
}
/**
* Revert the resource from a possibly existing backup file with the same basename
* and ending in .bak
* @throws Exception If anything goes wrong.
*/
public void revertFromBackupFile() throws Exception {
Configuration pluginConfiguration = getResourceContext().getPluginConfiguration();
String fullFileName = pluginConfiguration.getSimple(FILENAME_PLUGIN_CONFIG_PROP).getStringValue();
File backup;
String fileName;
// Check if we have an exploded war or not.
if (fullFileName != null && fullFileName.endsWith("/")) {
fileName = fullFileName.substring(0, fullFileName.length() - 1);
} else {
fileName = fullFileName;
}
backup = new File(fileName + ".bak");
if (!backup.exists()) {
throw new FileNotFoundException("Backup file " + backup + " does not exist");
}
File directory = backup.getParentFile();
if (!directory.canWrite()) {
throw new IOException("Can not modify directory " + directory);
}
File tmpBackup = new File(fileName + ".tmp.bak");
File file = new File(fullFileName);
boolean good = file.renameTo(tmpBackup);
if (!good) {
throw new IOException("Aborted as it is not possible to move to original to a tmp backup at " + tmpBackup);
}
// Now that we have moved the original to a backup, try to move the real backup in
good = backup.renameTo(file);
if (!good) {
// move backup in failed
good = tmpBackup.renameTo(file);
if (!good) {
throw new IOException("Installing backup " + backup + " failed and re-installing the original from "
+ tmpBackup + " failed too. Please correct this manually");
}
} else {
// installing from backup worked
FileUtils.purge(tmpBackup, true);
}
getParentResourceComponent().deployFile(file);
}
/**
* 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 moveTempFileToDeployLocation(File newApplication, File appFile, boolean isExploded) {
InputStream tempIs = null;
try {
if (isExploded) {
tempIs = new BufferedInputStream(new FileInputStream(newApplication));
appFile.mkdirs();
ZipUtil.unzipFile(tempIs, appFile);
} else {
newApplication.renameTo(appFile);
}
} catch (IOException e) {
log.error("Error writing updated package bits to the existing application location: " + appFile, e);
//return failApplicationDeployment("Error writing updated package bits to the existing application location: " +
// appFile, packageDetails);
} finally {
if (tempIs != null) {
try {
tempIs.close();
} catch (IOException e) {
log.error("Error closing temporary input stream", e);
}
}
}
}
private File writeNewAppBitsToTempFile(File file, ContentServices contentServices,
ResourcePackageDetails packageDetails) throws Exception {
File tempDir = getResourceContext().getTemporaryDirectory();
File tempFile = new File(tempDir.getAbsolutePath(), file.getName()
+ UUID.randomUUID().getLeastSignificantBits());
// The temp file shouldn't be there, but check and delete it if it is
if (tempFile.exists()) {
log.warn("Existing temporary file found and will be deleted at: " + tempFile);
tempFile.delete();
}
OutputStream tempOutputStream = null;
try {
tempOutputStream = new BufferedOutputStream(new FileOutputStream(tempFile));
contentServices.downloadPackageBits(getResourceContext().getContentContext(), packageDetails.getKey(),
tempOutputStream, true);
} 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;
}
private String getSHA256(File file) {
String sha256 = null;
try {
FileContentDelegate fileContentDelegate = new FileContentDelegate(file, null, null);
sha256 = fileContentDelegate.getSHA(file);
} catch (Exception iex) {
//log exception but move on, discovery happens often. No reason to hold up anything.
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);
}
}