/*
* 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.enterprise.server.core.plugin;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.rhq.core.clientapi.agent.metadata.PluginDependencyGraph;
import org.rhq.core.clientapi.agent.metadata.PluginMetadataManager;
import org.rhq.core.clientapi.descriptor.AgentPluginDescriptorUtil;
import org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.plugin.CannedGroupAddition;
import org.rhq.core.domain.plugin.Plugin;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.enterprise.server.resource.ResourceTypeManagerLocal;
import org.rhq.enterprise.server.resource.group.definition.GroupDefinitionManagerLocal;
import org.rhq.enterprise.server.resource.metadata.PluginManagerLocal;
import org.rhq.enterprise.server.system.SystemManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
/**
* ProductPlugin deployer responsible for detecting agent plugin jars on the filesystem.
*/
public class ProductPluginDeployer {
private Log log = LogFactory.getLog(ProductPluginDeployer.class.getName());
private File pluginDir = null;
private boolean isStarted = false;
private boolean isReady = false;
/** Map of plugin names to the corresponding plugins' deployment infos */
private Map<String, DeploymentInfo> deploymentInfos = new HashMap<String, DeploymentInfo>();
/** Map of plugin names to the corresponding plugins' versions */
private Map<String, ComparableVersion> pluginVersions = new HashMap<String, ComparableVersion>();
/** Set of plugins that have been accepted but need to be registered (useful during hot-deployment) */
private Set<String> namesOfPluginsToBeRegistered = new HashSet<String>();
/** Metadata cache for all JAXB plugin descriptors and resource types of all plugins */
private PluginMetadataManager metadataManager = new PluginMetadataManager();
public ProductPluginDeployer() {
// intentionally left blank
}
public File getPluginDir() {
return this.pluginDir;
}
public void setPluginDir(File pluginDir) {
this.pluginDir = pluginDir;
// this directory should always exist, but just in case it doesn't, create it
if (!this.pluginDir.exists()) {
this.pluginDir.mkdirs();
}
}
public PluginMetadataManager getPluginMetadataManager() {
return this.metadataManager;
}
/**
* This is called by the server's startup servlet which essentially informs us that
* the server's internal EJB/SLSBs are ready and can be called. This means we are
* allowed to begin registering types from deployed plugins.
*/
public void startDeployment() {
// we can now register our initial set of plugins (This may be a no-op at this point)
registerPlugins();
// indicate that we are now ready for hot-deployment of new plugins
this.isReady = true;
}
/**
* This is called when this deployer service itself is starting up.
*/
public void start() throws Exception {
if (!isStarted) {
isStarted = true;
}
}
public void stop() {
if (isStarted) {
this.deploymentInfos.clear();
this.pluginVersions.clear();
this.namesOfPluginsToBeRegistered.clear();
this.metadataManager = new PluginMetadataManager();
isStarted = false;
isReady = false;
}
}
/**
* This is called when a new or updated plugin is brought online.
* This just marks the plugin as being needed to be registered. Caller
* must ensure that {@link #registerPlugins()} is called afterwards
* to fully process the detected plugin.
*
* @param deploymentInfo information on the newly detected plugin
*/
public void pluginDetected(DeploymentInfo deploymentInfo) throws Exception {
if (!accepts(deploymentInfo)) {
return;
}
// don't cache deployment infos across starts, so if we've seen this deployment info before,
// take the current one we were just given and use it to replace the old info
String key = null;
for (Map.Entry<String, DeploymentInfo> entry : this.deploymentInfos.entrySet()) {
if (entry.getValue().equals(deploymentInfo)) {
key = entry.getKey();
break;
}
}
if (key != null) {
this.deploymentInfos.put(key, deploymentInfo);
}
String name = preprocessPlugin(deploymentInfo);
// isReady == true means startDeployer() has already been called, so this is a hot deploy.
// (if the EJB3 SLSBs are not ready yet, isReady will be false.
if (this.isReady) {
log.debug("Will hot deploy plugin [" + name + "] from [" + deploymentInfo.url + "]");
// do NOT register plugins yet - the dependency graph might not be complete, let the caller call registerPlugins
} else {
// startDeployer() has not been called yet so we are holding off registering until then
log.debug("Not ready yet - will deploy plugin [" + name + "] from [" + deploymentInfo.url + "] later");
}
return;
}
/**
* Determines if this is a plugin we should process.
*
* @param di the deployment information of the detected file (which is probably an agent plugin file)
* @return <code>true</code> if the deployment info represents an agent plugin file
*/
private boolean accepts(DeploymentInfo di) {
String urlString = di.url.getFile();
if (!urlString.endsWith(".jar")) {
return false;
}
File deploymentDirectory = new File(urlString).getParentFile();
if (deploymentDirectory.getName().equals(this.pluginDir.getName())) {
log.debug("accepting agent plugin=" + urlString);
return true;
}
return false;
}
/**
* Registers newly detected plugins and their types.
*
* Only call this method when {@link #isReady} is true. This is a no-op if we are not ready.
*/
public void registerPlugins() {
if (!this.isReady) {
return;
}
for (Iterator<String> it = this.namesOfPluginsToBeRegistered.iterator(); it.hasNext();) {
String pluginName = it.next();
if (!isNewOrUpdated(pluginName)) {
log.debug("Plugin [" + pluginName + "] has not been updated.");
it.remove();
}
}
if (this.namesOfPluginsToBeRegistered.isEmpty()) {
log.debug("All agent plugins were already up to date in the database.");
return;
}
Set<String> pluginsToBeRegistered = new HashSet<String>(this.namesOfPluginsToBeRegistered);
log.info("Deploying [" + pluginsToBeRegistered.size() + "] new or updated agent plugins: "
+ pluginsToBeRegistered);
PluginDependencyGraph dependencyGraph = buildDependencyGraph();
StringBuilder errorBuffer = new StringBuilder();
if (!dependencyGraph.isComplete(errorBuffer)) {
log.error(errorBuffer.toString());
if (log.isDebugEnabled()) {
log.debug(dependencyGraph.toString());
}
// reduce the graph down to only those plugins and their deps that exist and only register those
dependencyGraph = dependencyGraph.reduceGraph();
pluginsToBeRegistered.retainAll(dependencyGraph.getPlugins());
}
if (pluginsToBeRegistered.size() > 0) {
registerPlugins(dependencyGraph, pluginsToBeRegistered);
}
log.info("Plugin metadata updates are complete for [" + pluginsToBeRegistered.size() + "] plugins: "
+ pluginsToBeRegistered);
this.namesOfPluginsToBeRegistered.removeAll(pluginsToBeRegistered);
// load resource facets cache
try {
ResourceTypeManagerLocal typeManager = LookupUtil.getResourceTypeManager();
typeManager.reloadResourceFacetsCache();
} catch (Throwable t) {
log.error("Could not load ResourceFacets cache", t);
}
// Trigger vacuums on some tables as the initial deployment might have changed a lot of things.
// There are probably more tables involved though.
// First wait to give Hibernate a chance to close all transactions etc.
try {
Thread.sleep(2000L);
} catch (InterruptedException ignored) {
}
Subject superuser = LookupUtil.getSubjectManager().getOverlord();
SystemManagerLocal systemManager = LookupUtil.getSystemManager();
systemManager.vacuum(superuser, new String[] { "RHQ_MEASUREMENT_DEF", "RHQ_CONFIG_DEF", "RHQ_RESOURCE_TYPE",
"RHQ_RESOURCE_TYPE_PARENTS", Plugin.TABLE_NAME });
return;
}
/**
* Process the specified plugin jar to figure out the plugin name and version. If it is the only plugin with this
* name, or if it has the newest version among other plugins with the same name, then add it to our master set of
* plugins to be registered. Once all EJBs are started, {@link #startDeployment()} will be called and will take care
* of registering the plugins.
*/
private String preprocessPlugin(DeploymentInfo deploymentInfo) throws Exception {
File pluginFile = new File(deploymentInfo.url.getFile());
ensureDeploymentIsValid(pluginFile);
PluginDescriptor descriptor = getPluginDescriptor(deploymentInfo);
String pluginName = descriptor.getName();
boolean initialDeploy = !this.deploymentInfos.containsKey(pluginName);
ComparableVersion version;
version = AgentPluginDescriptorUtil.getPluginVersion(pluginFile, descriptor);
if (initialDeploy) {
log.info("Discovered agent plugin [" + pluginName + "]");
} else {
log.info("Rediscovered agent plugin [" + pluginName + "]");
}
if (initialDeploy || isNewestVersion(pluginName, version)) {
this.metadataManager.storePluginDescriptor(descriptor);
this.deploymentInfos.put(pluginName, deploymentInfo);
this.pluginVersions.put(pluginName, version);
this.namesOfPluginsToBeRegistered.add(pluginName);
}
return pluginName;
}
private PluginDescriptor getPluginDescriptor(DeploymentInfo di) throws Exception {
try {
PluginDescriptor pluginDescriptor = AgentPluginDescriptorUtil.loadPluginDescriptorFromUrl(di.url);
return pluginDescriptor;
} catch (Exception e) {
throw new Exception("Failed to parse descriptor found in plugin [" + di.url + "]", e);
}
}
private boolean isNewestVersion(String pluginName, ComparableVersion version) {
boolean newestVersion;
ComparableVersion existingVersion = this.pluginVersions.get(pluginName);
if (existingVersion != null) {
newestVersion = (version.compareTo(existingVersion) >= 0);
if (newestVersion)
log.info("Newer version of [" + pluginName + "] plugin found (version " + version
+ ") - older version (" + existingVersion + ") will be ignored.");
} else {
newestVersion = false;
}
return newestVersion;
}
private boolean isNewOrUpdated(String pluginName) {
DeploymentInfo deploymentInfo = this.deploymentInfos.get(pluginName);
if (deploymentInfo == null) {
throw new IllegalStateException("DeploymentInfo was not found for plugin [" + pluginName
+ " ] - it should have been initialized by preprocessPlugin().");
}
PluginManagerLocal pluginMgr = LookupUtil.getPluginManager();
Plugin plugin = pluginMgr.getPlugin(pluginName);
if (null == plugin) {
log.debug("New plugin [" + pluginName + "] detected.");
return true;
}
String md5 = null;
try {
md5 = MessageDigestGenerator.getDigestString(new File(deploymentInfo.url.toURI()));
} catch (Exception e) {
log.error("Error generating MD5 for plugin [" + pluginName + "]. Cause: " + e);
}
if (!plugin.getMd5().equals(md5)) {
log.debug("Updated plugin [" + pluginName + "] detected.");
return true;
}
return false;
}
private PluginDependencyGraph buildDependencyGraph() {
PluginDependencyGraph dependencyGraph = new PluginDependencyGraph();
for (String pluginName : this.deploymentInfos.keySet()) {
PluginDescriptor descriptor = this.metadataManager.getPluginDescriptor(pluginName);
AgentPluginDescriptorUtil.addPluginToDependencyGraph(dependencyGraph, descriptor);
}
return dependencyGraph;
}
private void registerPlugins(PluginDependencyGraph dependencyGraph, Set<String> pluginsToBeRegistered) {
log.debug("Dependency graph deployment order: " + dependencyGraph.getDeploymentOrder());
Map<String, DeploymentRunnable> dependencyRunnableMap = new HashMap<String, DeploymentRunnable>();
for (String pluginName : pluginsToBeRegistered) {
DeploymentRunnable service = getServiceIfExists(pluginName, dependencyRunnableMap);
if (service == null) {
log.warn("Cannot create the initial deployment runnable for plugin [" + pluginName + "]");
}
// We need to register dependencies also even if they aren't new or updated. This is because
// PluginMetadataManager requires dependency plugins to be loaded in its pluginsByParser map.
// ResourceMetadataManagerBean.register() will be smart enough to pass these plugins to
// PluginMetadataManager to be parsed, but not to unnecessarily merge their types into the DB.
for (String dependencyPluginName : dependencyGraph.getPluginDependencies(pluginName)) {
DeploymentRunnable dependencyService = getServiceIfExists(dependencyPluginName, dependencyRunnableMap);
if (null == dependencyService) {
log.warn("Ignoring [" + pluginName + "] dependency on missing dependency plugin: "
+ dependencyPluginName);
}
}
// In addition, we need to register plugins that are optionally dependent on the plugins we must register
// in order to allow the dependents to refresh themselves and add any new child types that need to be registered.
List<String> optionalDependents = dependencyGraph.getOptionalDependents(pluginName);
for (String dependentPluginName : optionalDependents) {
DeploymentRunnable dependentService = getServiceIfExists(dependentPluginName, dependencyRunnableMap);
if (null != dependentService) {
dependentService.setForceUpdate(true); // make sure it updates its types, even if plugin hasn't changed
} else {
log.warn("Ignoring [" + pluginName + "] dependent on missing dependent plugin: "
+ dependentPluginName);
}
}
}
// get the order in which they should be deployed
ArrayList<DeploymentRunnable> orderedDeploymentRunnables = new ArrayList<DeploymentRunnable>();
List<String> pluginOrder = dependencyGraph.getDeploymentOrder();
for (String nextPlugin : pluginOrder) {
DeploymentRunnable nextRunnable = dependencyRunnableMap.get(nextPlugin);
if (nextRunnable != null) {
orderedDeploymentRunnables.add(nextRunnable);
}
}
// now do the actual deployments in the correct order
long startDeployTime = System.currentTimeMillis();
for (DeploymentRunnable currentRunnable : orderedDeploymentRunnables) {
currentRunnable.run();
}
long endDeployTime = System.currentTimeMillis();
log.debug("Registered [" + pluginsToBeRegistered.size() + "] plugins in [" + (endDeployTime - startDeployTime)
+ "]ms");
}
// Who needs this???
/*
private List<String> getRegisteredPluginNames() {
ResourceMetadataManagerLocal metadataManager = LookupUtil.getResourceMetadataManager();
Collection<Plugin> plugins = metadataManager.getInstalledPlugins();
List<String> pluginNames = new ArrayList<String>();
for (Plugin plugin : plugins) {
pluginNames.add(plugin.getName());
}
return pluginNames;
}
*/
/**
* This will return the deployment runnable for the associated plugin.
* This will create a DeploymentRunnable if one doesn't yet exist.
* If it can't create one, null is returned.
*
* @param pluginName
* @param runnableMap
* @return the deployment runnable that can be used to deploy the plugin; null if not able to create one
*/
private DeploymentRunnable getServiceIfExists(String pluginName, Map<String, DeploymentRunnable> runnableMap) {
DeploymentRunnable result = runnableMap.get(pluginName);
if (result == null) {
DeploymentInfo deploymentInfo = this.deploymentInfos.get(pluginName);
PluginDescriptor descriptor = this.metadataManager.getPluginDescriptor(pluginName);
CannedGroupAddition addition = PluginAdditionsReader.getCannedGroupsAddition(deploymentInfo.url, pluginName);
if ((null != deploymentInfo) && (null != descriptor)) {
result = new DeploymentRunnable(pluginName, deploymentInfo, descriptor, addition);
runnableMap.put(pluginName, result);
}
}
return result;
}
/**
* This is the mechanism to kick off the registration of a new plugin. You must ensure you call this at the
* appropriate time such that the plugin getting registered already has its dependencies registered.
*/
private void registerPluginJar(PluginDescriptor pluginDescriptor, CannedGroupAddition addition, DeploymentInfo deploymentInfo, boolean forceUpdate) {
if (pluginDescriptor == null) {
log.error("Missing plugin descriptor; is [" + deploymentInfo.url + "] a valid plugin?");
return;
}
try {
File localPluginFile = new File(deploymentInfo.url.toURI());
String pluginName = pluginDescriptor.getName();
String displayName = pluginDescriptor.getDisplayName();
String pluginNameDisplayName = pluginName + " (" + displayName + ")";
ComparableVersion comparableVersion = this.pluginVersions.get(pluginName);
String version = (comparableVersion != null) ? comparableVersion.toString() : null;
log.debug("Registering RHQ plugin " + pluginNameDisplayName + ", "
+ ((version != null) ? "version " + version : "undefined version") + "...");
checkVersionCompatibility(pluginDescriptor.getAmpsVersion());
String filename = getPluginJarFilename(deploymentInfo); // make sure this is only the filename
Plugin plugin = new Plugin(pluginName, filename);
plugin.setDisplayName((displayName != null) ? displayName : pluginName);
plugin.setEnabled(true);
plugin.setDescription(pluginDescriptor.getDescription());
plugin.setAmpsVersion(getAmpsVersion(pluginDescriptor));
// get the last modified of the "real" plugin jar since that's the one the user touches
long mtime = deploymentInfo.url.openConnection().getLastModified();
plugin.setMtime(mtime);
if (pluginDescriptor.getHelp() != null && !pluginDescriptor.getHelp().getContent().isEmpty()) {
plugin.setHelp(String.valueOf(pluginDescriptor.getHelp().getContent().get(0)));
}
plugin.setVersion(version);
plugin.setMD5(MessageDigestGenerator.getDigestString(localPluginFile));
// this manager is responsible for handling the munging of plugins that depend on other plugins
// since we assume we are called in the proper deployment order, this should not fail
// if we are called when hot-deploying a plugin whose dependencies aren't deployed, this will fail
PluginManagerLocal pluginMgr = LookupUtil.getPluginManager();
pluginMgr.registerPlugin(plugin, pluginDescriptor, localPluginFile, forceUpdate);
if (addition!=null) {
GroupDefinitionManagerLocal groupDefMgr = LookupUtil.getGroupDefinitionManager();
groupDefMgr.updateGroupsByCannedExpressions(pluginName, addition.getExpressions());
}
} catch (Exception e) {
log.error("Failed to register RHQ plugin file [" + deploymentInfo.url + "]", e);
}
}
private String getAmpsVersion(PluginDescriptor pluginDescriptor) {
if (pluginDescriptor.getAmpsVersion() == null) {
return "2.0";
}
ComparableVersion version = new ComparableVersion(pluginDescriptor.getAmpsVersion());
ComparableVersion version2 = new ComparableVersion("2.0");
if (version.compareTo(version2) <= 0) {
return "2.0";
}
return pluginDescriptor.getAmpsVersion();
}
private void checkVersionCompatibility(String version) throws RuntimeException {
/*if (new OSGiVersionComparator().compare((String) version, (String) AMPS_VERSION) < 0)
* { throw new RuntimeException("Plugin AMPS requirement " + version + " not compatible with server's AMPS
* version " + AMPS_VERSION);}*/
}
private void ensureDeploymentIsValid(File pluginFile) throws Exception {
// try a few times (sleeping between retries)
// if the zip file still isn't valid, its probably corrupted and not simply due to the file still being written out
int retries = 4;
while (!isDeploymentValidZipFile(pluginFile)) {
if (--retries <= 0) {
throw new Exception("File [" + pluginFile + "] is not a valid jarfile - "
+ " it is either corrupted or file has not been fully written yet.");
}
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
break;
}
}
return;
}
private boolean isDeploymentValidZipFile(File pluginFile) {
boolean isValid;
JarFile jarFile = null;
try {
// Try to access the plugin jar using the JarFile API.
// Any weird errors usually mean the file is currently being written but isn't finished yet.
// Errors could also mean the file is simply corrupted.
jarFile = new JarFile(pluginFile);
if (jarFile.size() <= 0) {
throw new Exception("There are no entries in the plugin file");
}
JarEntry entry = jarFile.entries().nextElement();
entry.getName();
isValid = true;
} catch (Exception e) {
log.info("File [" + pluginFile + "] is not a valid jarfile - "
+ " the file may not have been fully written yet. Cause: " + e);
isValid = false;
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (Exception e) {
log.error("Failed to close jar file [" + pluginFile + "]");
}
}
}
return isValid;
}
/**
* This returns the name of the plugin file that is represented by the given
* deployment info. This returns just the name of the plugin file, without
* any parent directory information.
*
* @param di the deployment info of the plugin file that is deployed
* @return the name of the plugin file
*/
private String getPluginJarFilename(DeploymentInfo di) {
return new File(di.url.getPath()).getName();
}
class DeploymentRunnable implements Runnable {
private final DeploymentInfo pluginDeploymentInfo;
private final PluginDescriptor pluginDescriptor;
private final CannedGroupAddition cgAddition;
private final String pluginName;
private boolean forceUpdate;
public DeploymentRunnable(String pluginName, DeploymentInfo di, PluginDescriptor descriptor, CannedGroupAddition cgAddition) {
this.pluginName = pluginName;
this.pluginDeploymentInfo = di;
this.pluginDescriptor = descriptor;
this.cgAddition = cgAddition;
this.forceUpdate = false;
}
public void setForceUpdate(boolean forceUpdate) {
this.forceUpdate = forceUpdate;
}
@Override
public void run() {
log.debug("Being asked to deploy plugin [" + this.pluginName + "]...");
registerPluginJar(this.pluginDescriptor, this.cgAddition, this.pluginDeploymentInfo, this.forceUpdate);
}
}
static class DeploymentInfo {
public final URL url;
public DeploymentInfo(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
this.url = url;
}
@Override
public int hashCode() {
return url.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || (!(obj instanceof DeploymentInfo))) {
return false;
}
return url.equals(((DeploymentInfo) obj).url);
}
}
}