modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataManager.java | 50 +++ modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataParser.java | 78 +++++ modules/core/client-api/src/main/resources/rhq-plugin.xsd | 46 +++ modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/MetadataManagerTest.java | 22 + modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/PluginMetadataParserTest.java | 83 ++++++ modules/core/client-api/src/test/resources/metadata-manager-test-2.xml | 7 modules/core/client-api/src/test/resources/metadata-manager-test-3.xml | 9 modules/core/plugin-api/src/main/java/org/rhq/core/pluginapi/inventory/ResourceDiscoveryCallback.java | 43 +++ modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/DiscoveryCallbackTest.java | 135 ++++++++++ modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginOneCallback.java | 22 + modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback1.java | 21 + modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback2.java | 19 + modules/core/plugin-container-itest/src/test/resources/discovery-callback1.xml | 21 + modules/core/plugin-container-itest/src/test/resources/discovery-callback2.xml | 24 + modules/core/plugin-container/src/main/java/org/rhq/core/pc/inventory/InventoryManager.java | 47 +++ modules/core/plugin-container/src/main/java/org/rhq/core/pc/plugin/PluginComponentFactory.java | 24 + 16 files changed, 643 insertions(+), 8 deletions(-)
New commits: commit af523918b7e179c15bcae948cb8e7102e5072732 Author: John Mazzitelli mazz@redhat.com Date: Fri Aug 30 15:16:58 2013 -0400
BZ 988735 - discovery callbacks that allow extension plugins to alter discovered details from some resource type
diff --git a/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataManager.java b/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataManager.java index e75b7df..b9ee838 100644 --- a/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataManager.java +++ b/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataManager.java @@ -82,6 +82,11 @@ public class PluginMetadataManager { private Set<ResourceType> ignoredResourceTypes = null; private Object disabledIgnoredTypesLock = new Object(); // used when accessing disabled and ignored collections
+ // these define the discovery callbacks per resource type. The key is the resource type whose discovered details + // need to be funneled through callbacks. The value is a map whose key is plugin names and whose values are + // discovery callback implementation classes defined in the plugins. + private Map<ResourceType, Map<String, List<String>>> discoveryCallbacks = new HashMap<ResourceType, Map<String, List<String>>>(); + public PluginMetadataManager() { }
@@ -203,6 +208,18 @@ public class PluginMetadataManager { findDisabledResourceTypesInAllPlugins(); }
+ // squirrel away all the discovery callbacks + Map<ResourceType, List<String>> discoveryCallbacksMap = parser.getDiscoveryCallbackClasses(); + if (discoveryCallbacksMap != null) { + for (Map.Entry<ResourceType, List<String>> entry : discoveryCallbacksMap.entrySet()) { + ResourceType resourceType = entry.getKey(); + for (String className : entry.getValue()) { + addDiscoveryCallbackClassName(resourceType, pluginDescriptor.getName(), className); + } + } + } + + // return the top root types from the descriptor Set<ResourceType> rootTypes = parser.getRootResourceTypes(); return rootTypes;
@@ -491,4 +508,37 @@ public class PluginMetadataManager { } return; } + + /** + * Given a resource type, this will return any discovery callbacks that are required to be invoked + * when that resource type is being discovered. + * @param resourceType the type whose discovery callbacks should be returned + * @return the collection of callbacks, grouped by the plugins that defined them (may be null) + */ + public Map<String, List<String>> getDiscoveryCallbacks(ResourceType resourceType) { + synchronized (discoveryCallbacks) { + Map<String, List<String>> map = discoveryCallbacks.get(resourceType); + return map; + } + } + + private void addDiscoveryCallbackClassName(ResourceType resourceType, String pluginName, String className) { + synchronized (discoveryCallbacks) { + Map<String, List<String>> map = discoveryCallbacks.get(resourceType); + if (map == null) { + map = new HashMap<String, List<String>>(1); + discoveryCallbacks.put(resourceType, map); + } + + List<String> callbackListForPlugin = map.get(pluginName); + if (callbackListForPlugin == null) { + callbackListForPlugin = new ArrayList<String>(1); + map.put(pluginName, callbackListForPlugin); + } + + callbackListForPlugin.add(className); + } + + return; + } } diff --git a/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataParser.java b/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataParser.java index d5ff2ef..f1bce29 100644 --- a/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataParser.java +++ b/modules/core/client-api/src/main/java/org/rhq/core/clientapi/agent/metadata/PluginMetadataParser.java @@ -37,6 +37,8 @@ import org.apache.commons.logging.LogFactory; import org.rhq.core.clientapi.descriptor.plugin.Bundle; import org.rhq.core.clientapi.descriptor.plugin.BundleTargetDescriptor; import org.rhq.core.clientapi.descriptor.plugin.ContentDescriptor; +import org.rhq.core.clientapi.descriptor.plugin.DiscoveryCallbacksType; +import org.rhq.core.clientapi.descriptor.plugin.DiscoveryTypeCallbackType; import org.rhq.core.clientapi.descriptor.plugin.DriftDescriptor; import org.rhq.core.clientapi.descriptor.plugin.EventDescriptor; import org.rhq.core.clientapi.descriptor.plugin.MetricDescriptor; @@ -83,10 +85,11 @@ public class PluginMetadataParser {
private Set<ResourceType> rootResourceTypes = new LinkedHashSet<ResourceType>();
- // TODO: this isn't the most elegant... should we put these in the domain objects? or perhaps build another place for them to live? private Map<ResourceType, String> discoveryClasses = new HashMap<ResourceType, String>(); private Map<ResourceType, String> componentClasses = new HashMap<ResourceType, String>();
+ private Map<ResourceType, List<String>> discoveryCallbackClasses = null; + // a map keyed on plugin name that contains the parsers for all other known plugin descriptors // this map is managed by this parser's PluginMetadataManager and is how the manager shares information // from other plugins to this parser instance @@ -120,6 +123,20 @@ public class PluginMetadataParser { return this.rootResourceTypes; }
+ /** + * This returns all resource types that this plugin defines discovery callbacks for. + * When the resource types' discovery is run, the details will be funneled through the discovery + * callbacks to give this plugin a chance to alter the discovered details. + * Note that it is very possible that the resource type keys are types that are NOT defined + * by the plugin associated with this parser. The resource types will be defined either in + * this plugin or one of its dependencies. + * + * @return map of all types that have one or more discovery callbacks defined. May be null. + */ + public Map<ResourceType, List<String>> getDiscoveryCallbackClasses() { + return discoveryCallbackClasses; + } + public void parseDescriptor() throws InvalidPluginDescriptorException { ResourceType type;
@@ -147,6 +164,65 @@ public class PluginMetadataParser { } }
+ // find any declared discovery callbacks now - do this at the end in + // case we are defining callbacks on types in our own plugin + parseDiscoveryCallbacks(); + + return; + } + + private void parseDiscoveryCallbacks() throws InvalidPluginDescriptorException { + DiscoveryCallbacksType jaxbCallbacks = pluginDescriptor.getDiscoveryCallbacks(); + if (jaxbCallbacks == null) { + return; + } + + List<DiscoveryTypeCallbackType> jaxbCallbacksList = jaxbCallbacks.getTypeCallback(); + if (jaxbCallbacksList == null || jaxbCallbacksList.isEmpty()) { + return; + } + + for (DiscoveryTypeCallbackType jaxbCallback : jaxbCallbacksList) { + String plugin = jaxbCallback.getPlugin(); + String type = jaxbCallback.getType(); + String callbackClass = jaxbCallback.getCallbackClass(); + + LOG.debug("Plugin [" + pluginDescriptor.getName() + "] defined a discovery class [" + callbackClass + + "] to listen for discovery details for type [{" + plugin + "}" + type + "]."); + + if (callbackClass == null || callbackClass.length() == 0) { + // this should never happen - the XML parser should have failed to even get this far + throw new InvalidPluginDescriptorException("Missing discovery class in plugin [" + + pluginDescriptor.getName() + "] -> {" + plugin + "}" + type); + } + + if (plugin == null || plugin.length() == 0 || type == null || type.length() == 0) { + // this should never happen - the XML parser should have failed to even get this far + throw new InvalidPluginDescriptorException("Both plugin and type must be defined for discovery callbacks in plugin [" + + pluginDescriptor.getName() + "] -> {" + plugin + "}" + type + ":" + callbackClass); + } + + ResourceType resourceType = getResourceTypeFromPlugin(type, plugin); + if (resourceType == null) { + LOG.warn("There is no type named [" + type + "] from a plugin named [" + plugin + + "]. This is probably because that plugin is missing. The discovery callback will be ignored"); + continue; + } + + if (discoveryCallbackClasses == null) { + discoveryCallbackClasses = new HashMap<ResourceType, List<String>>(); + } + + List<String> callbacksList = discoveryCallbackClasses.get(resourceType); + if (callbacksList == null) { + callbacksList = new ArrayList<String>(1); + discoveryCallbackClasses.put(resourceType, callbacksList); + } + + String fqcn = getFullyQualifiedComponentClassName(pluginDescriptor.getPackage(), callbackClass); + callbacksList.add(fqcn); + } + return; }
diff --git a/modules/core/client-api/src/main/resources/rhq-plugin.xsd b/modules/core/client-api/src/main/resources/rhq-plugin.xsd index 2d50b8e..ee873aa 100644 --- a/modules/core/client-api/src/main/resources/rhq-plugin.xsd +++ b/modules/core/client-api/src/main/resources/rhq-plugin.xsd @@ -57,6 +57,18 @@ </xs:complexType> </xs:element>
+ <xs:element ref="rhq:discovery-callbacks" minOccurs="0" maxOccurs="1"> + xs:annotation + xs:documentation + Defines the types and discovery classes whose discovered resources must be + funneled through this plugin's discovery classback classes. + </xs:documentation> + xs:appinfo + <jaxb:property name="discoveryCallbacks" /> + </xs:appinfo> + </xs:annotation> + </xs:element> + <xs:element ref="rhq:platform" minOccurs="0" maxOccurs="unbounded"> xs:annotation xs:documentation @@ -1154,4 +1166,38 @@ </xs:attribute> </xs:attributeGroup>
+ <xs:element name="discovery-callbacks" type="rhq:DiscoveryCallbacksType"> + xs:annotation + xs:documentationA set of discovery callbacks this plugin defines to help with resource discovery.</xs:documentation> + </xs:annotation> + </xs:element> + + <xs:complexType name="DiscoveryCallbacksType"> + xs:sequence + <xs:element name="type-callback" type="rhq:DiscoveryTypeCallbackType" maxOccurs="unbounded" minOccurs="1"> + xs:annotation + xs:documentationA discovery callback to help with discovery for a specific resource type.</xs:documentation> + </xs:annotation> + </xs:element> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="DiscoveryTypeCallbackType"> + <xs:attribute name="type" type="xs:string" use="required"> + xs:annotation + xs:documentationWhen a discovery component detects this resource type, the discovered details will be processed by the discovery callback.</xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="plugin" type="xs:string" use="required"> + xs:annotation + xs:documentationThe plugin that defines the named resource type. Must be either this plugin or one of its dependencies.</xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="callbackClass" type="rhq:classNameType" use="required"> + xs:annotation + xs:documentationThe class name of this discovery callback implementation.</xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:schema> diff --git a/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/MetadataManagerTest.java b/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/MetadataManagerTest.java index fbb6b1b..c6f3dc2 100644 --- a/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/MetadataManagerTest.java +++ b/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/MetadataManagerTest.java @@ -23,6 +23,7 @@ package org.rhq.core.clientapi.agent.metadata.test;
import java.net.URL; +import java.util.List; import java.util.Map; import java.util.Set;
@@ -177,6 +178,27 @@ public class MetadataManagerTest { assert testD.getBundleType().getResourceType().equals(testD); }
+ @Test(dependsOnMethods = "loadPluginDescriptorTest3") + public void testDiscoveryCallbackDefinitions() { + ResourceType serverAType = this.metadataManager.getType("Server A", "Test1"); + ResourceType serverBType = this.metadataManager.getType("Extension Server B", "Test2"); + assert serverAType != null : "Where's Server A?"; + assert serverBType != null : "Where's Extension Server B?"; + + Map<String, List<String>> serverACallbacks = this.metadataManager.getDiscoveryCallbacks(serverAType); + assert serverACallbacks.size() == 1 : serverACallbacks; + assert serverACallbacks.get("Test3").size() == 2 : serverACallbacks; + assert serverACallbacks.get("Test3").get(0).equals("org.rhq.plugins.test3.DiscoveryCallback1") : serverACallbacks; + assert serverACallbacks.get("Test3").get(1).equals("org.rhq.plugins.test3.DiscoveryCallbackAnother1") : serverACallbacks; + + Map<String, List<String>> serverBCallbacks = this.metadataManager.getDiscoveryCallbacks(serverBType); + assert serverBCallbacks.size() == 2 : serverBCallbacks; + assert serverBCallbacks.get("Test3").size() == 1 : serverBCallbacks; + assert serverBCallbacks.get("Test2").size() == 1 : serverBCallbacks; + assert serverBCallbacks.get("Test3").get(0).equals("org.rhq.plugins.test3.DiscoveryCallback2") : serverBCallbacks; + assert serverBCallbacks.get("Test2").get(0).equals("org.rhq.plugins.test2.DiscoveryCallbackTest2") : serverBCallbacks; + } + private ResourceType getResourceType(ResourceType typeToGet) { for (ResourceType type : metadataManager.getAllTypes()) { if (type.equals(typeToGet)) { diff --git a/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/PluginMetadataParserTest.java b/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/PluginMetadataParserTest.java index 5e08676..1c86934 100644 --- a/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/PluginMetadataParserTest.java +++ b/modules/core/client-api/src/test/java/org/rhq/core/clientapi/agent/metadata/test/PluginMetadataParserTest.java @@ -51,13 +51,94 @@ import static org.testng.Assert.assertTrue; public class PluginMetadataParserTest {
@Test + void discoveryCallback() throws Exception { + PluginDescriptor pluginDescriptor = toPluginDescriptor("" + // + "<plugin name='TestServer' displayName='Test Server' package='org.rhq.plugins.test'" + // + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" + // + " xmlns='urn:xmlns:rhq-plugin'>" + // + " <discovery-callbacks>" + // + " <type-callback type='testServerType' plugin='TestServer' callbackClass='TestCallback' />" + // + " </discovery-callbacks>" + // + " <server name='testServerType'" + // + " class='org.rhq.plugins.test.TestServer'" + // + " discovery='org.rhq.plugins.test.TestServerDiscoveryComponent'/>" + // + "</plugin>"); + + Map<String, PluginMetadataParser> parsersByPlugin = new HashMap<String, PluginMetadataParser>(0); + PluginMetadataParser parser = new PluginMetadataParser(pluginDescriptor, parsersByPlugin); + + Map<ResourceType, List<String>> map = parser.getDiscoveryCallbackClasses(); + assertEquals(map.size(), 1, "Should have one discovery callback: " + map); + ResourceType rt = new ResourceType("testServerType", "TestServer", null, null); + assertEquals(map.get(rt).get(0), "org.rhq.plugins.test.TestCallback", "incorrect classname in map: " + map); + + // in a second plugin, define a discovery callback that points back to a type in the first plugin + PluginDescriptor pluginDescriptor2 = toPluginDescriptor("" + // + "<plugin name='TestServer2' displayName='Test Server' package='org.rhq.plugins.test2'" + // + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" + // + " xmlns='urn:xmlns:rhq-plugin'>" + // + " <discovery-callbacks>" + // + " <type-callback type='testServerType' plugin='TestServer' callbackClass='TestCallback2' />" + // + " </discovery-callbacks>" + // + " <server name='testServerType2'" + // + " class='org.rhq.plugins.test2.TestServer2'" + // + " discovery='org.rhq.plugins.test2.TestServerDiscoveryComponent2'/>" + // + "</plugin>"); + + parsersByPlugin.put("TestServer", parser); + PluginMetadataParser parser2 = new PluginMetadataParser(pluginDescriptor2, parsersByPlugin); + map = parser2.getDiscoveryCallbackClasses(); + assertEquals(map.size(), 1, "Should have one discovery callback: " + map); + rt = new ResourceType("testServerType", "TestServer", null, null); + assertEquals(map.get(rt).get(0), "org.rhq.plugins.test2.TestCallback2", "incorrect classname in map: " + map); + + // define multiple callbacks to multiple plugins + PluginDescriptor pluginDescriptor3 = toPluginDescriptor("" + // + "<plugin name='TestServer3' displayName='Test Server' package='org.rhq.plugins.test3'" + // + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" + // + " xmlns='urn:xmlns:rhq-plugin'>" + // + " <discovery-callbacks>" + // + " <type-callback type='testServerType' plugin='TestServer' callbackClass='TestCallback31' />" + // + " <type-callback type='testServerType2' plugin='TestServer2' callbackClass='TestCallback32' />" + // + " <type-callback type='testServerType3' plugin='TestServer3' callbackClass='TestCallback33' />" + // + " <type-callback type='testServerType3' plugin='TestServer3' callbackClass='TestCallback34' />" + // + " </discovery-callbacks>" + // + " <server name='testServerType3'" + // + " class='org.rhq.plugins.test3.TestServer3'" + // + " discovery='org.rhq.plugins.test3.TestServerDiscoveryComponent3'/>" + // + "</plugin>"); + + parsersByPlugin.put("TestServer2", parser2); + PluginMetadataParser parser3 = new PluginMetadataParser(pluginDescriptor3, parsersByPlugin); + map = parser3.getDiscoveryCallbackClasses(); + assertEquals(map.size(), 3, "Should have three keys in discovery callbacks map: " + map); + + rt = new ResourceType("testServerType", "TestServer", null, null); + List<String> list = map.get(rt); + assertEquals(list.size(), 1); + assertEquals(list.get(0), "org.rhq.plugins.test3.TestCallback31", "incorrect classname in map: " + map); + + rt = new ResourceType("testServerType2", "TestServer2", null, null); + list = map.get(rt); + assertEquals(list.size(), 1); + assertEquals(list.get(0), "org.rhq.plugins.test3.TestCallback32", "incorrect classname in map: " + map); + + rt = new ResourceType("testServerType3", "TestServer3", null, null); + list = map.get(rt); + assertEquals(list.size(), 2); + assertEquals(list.get(0), "org.rhq.plugins.test3.TestCallback33", "incorrect classname in map: " + map); + assertEquals(list.get(1), "org.rhq.plugins.test3.TestCallback34", "incorrect classname in map: " + map); + + } + + @Test void allTypesShouldHaveOneElementForDescriptorWithOnlyOneResourceType() throws Exception { PluginDescriptor pluginDescriptor = toPluginDescriptor("" + // "<plugin name='TestServer' displayName='Test Server' package='org.rhq.plugins.test'" + // " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" + // " xmlns='urn:xmlns:rhq-plugin'>" + // " <server name='testServer'" + // - " class='org.rhq.plugins.test.TestServer'" + // + " class='org.rhq.plugins.test.TestServer'" + // " discovery='org.rhq.plugins.test.TestServerDiscoveryComponent'/>" + // "</plugin>");
diff --git a/modules/core/client-api/src/test/resources/metadata-manager-test-2.xml b/modules/core/client-api/src/test/resources/metadata-manager-test-2.xml index f19b2f4..144ffcc 100644 --- a/modules/core/client-api/src/test/resources/metadata-manager-test-2.xml +++ b/modules/core/client-api/src/test/resources/metadata-manager-test-2.xml @@ -3,7 +3,12 @@ xmlns="urn:xmlns:rhq-plugin">
<depends plugin="Test1" /> - <server name="Extension Server B" discovery="ServerBComponent" class="ServerBServerComponent" + + <discovery-callbacks> + <type-callback type="Extension Server B" plugin="Test2" callbackClass="DiscoveryCallbackTest2"/> + </discovery-callbacks> + + <server name="Extension Server B" discovery="ServerBComponent" class="ServerBServerComponent" description="Extension Server B Test Server" sourcePlugin="Test1" sourceType="Server A">
diff --git a/modules/core/client-api/src/test/resources/metadata-manager-test-3.xml b/modules/core/client-api/src/test/resources/metadata-manager-test-3.xml index 48cd715..41907cc 100644 --- a/modules/core/client-api/src/test/resources/metadata-manager-test-3.xml +++ b/modules/core/client-api/src/test/resources/metadata-manager-test-3.xml @@ -1,9 +1,16 @@ -<plugin name="Test3" displayName="Test Two" package="org.rhq.plugins.test2" +<plugin name="Test3" displayName="Test Three" package="org.rhq.plugins.test3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:xmlns:rhq-plugin">
<depends plugin="Test1" /> <depends plugin="Test2" /> + + <discovery-callbacks> + <type-callback type="Server A" plugin="Test1" callbackClass="DiscoveryCallback1"/> + <type-callback type="Server A" plugin="Test1" callbackClass="DiscoveryCallbackAnother1"/> + <type-callback type="Extension Server B" plugin="Test2" callbackClass="DiscoveryCallback2"/> + </discovery-callbacks> + <server name="Injection C To Server A" discovery="ServerCComponent" class="ServerCServerComponent" description="injects child C to server A and server B"> <runs-inside> diff --git a/modules/core/plugin-api/src/main/java/org/rhq/core/pluginapi/inventory/ResourceDiscoveryCallback.java b/modules/core/plugin-api/src/main/java/org/rhq/core/pluginapi/inventory/ResourceDiscoveryCallback.java new file mode 100644 index 0000000..a1360af --- /dev/null +++ b/modules/core/plugin-api/src/main/java/org/rhq/core/pluginapi/inventory/ResourceDiscoveryCallback.java @@ -0,0 +1,43 @@ +/* + * RHQ Management Platform + * Copyright (C) 2005-2013 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.core.pluginapi.inventory; + +import java.util.Set; + +/** + * When another discovery component discovers resources, the discovered details can be funneled through + * implementations of this callback interface, thus allowing callbacks to tweek details of discovered resources. + * This is helpful, for example, when one plugin wants to alter the name or description of some other plugin's + * resource type. This is mainly used when writing plugins that cooperate with each other. + */ +public interface ResourceDiscoveryCallback { + /** + * When a set of resource details have been discovered, those details are passed to the callback via this method. + * The callback can tweek those details as it sees fit or it can simply leave the details as-is and simply return. + * + * @param discoveredDetails a set of resource details that were discovered and can be altered by the callback + * + * @throws Exception + */ + void discoveredResources(Set<DiscoveredResourceDetails> discoveredDetails) throws Exception; +} \ No newline at end of file diff --git a/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/DiscoveryCallbackTest.java b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/DiscoveryCallbackTest.java new file mode 100644 index 0000000..4b4b985 --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/DiscoveryCallbackTest.java @@ -0,0 +1,135 @@ +/* + * RHQ Management Platform + * Copyright (C) 2012 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.core.pc.inventory; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.TargetsContainer; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.arquillian.testng.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.rhq.core.clientapi.server.discovery.InventoryReport; +import org.rhq.core.domain.resource.InventoryStatus; +import org.rhq.core.domain.resource.Resource; +import org.rhq.core.pc.PluginContainer; +import org.rhq.core.pc.PluginContainerConfiguration; +import org.rhq.core.pc.inventory.discoverycallback.PluginOneCallback; +import org.rhq.core.pc.inventory.discoverycallback.PluginTwoCallback1; +import org.rhq.core.pc.inventory.discoverycallback.PluginTwoCallback2; +import org.rhq.core.pc.inventory.testplugin.TestResourceComponent; +import org.rhq.core.pc.inventory.testplugin.TestResourceDiscoveryComponent; +import org.rhq.test.arquillian.AfterDiscovery; +import org.rhq.test.arquillian.BeforeDiscovery; +import org.rhq.test.arquillian.FakeServerInventory; +import org.rhq.test.arquillian.MockingServerServices; +import org.rhq.test.arquillian.RunDiscovery; +import org.rhq.test.shrinkwrap.RhqAgentPluginArchive; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Set; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +/** + * A unit test for testing discovery callbacks. + */ +public class DiscoveryCallbackTest extends Arquillian { + + @Deployment(name = "test") + @TargetsContainer("pc") + public static RhqAgentPluginArchive getTestOnePlugin() { + RhqAgentPluginArchive pluginJar1 = ShrinkWrap.create(RhqAgentPluginArchive.class, "test-discovery-callback1-plugin.jar"); + RhqAgentPluginArchive pluginJar2 = ShrinkWrap.create(RhqAgentPluginArchive.class, "test-discovery-callback2-plugin.jar"); + pluginJar1.setPluginDescriptor("discovery-callback1.xml").addClasses( + TestResourceDiscoveryComponent.class, TestResourceComponent.class, PluginOneCallback.class); + pluginJar2.setPluginDescriptor("discovery-callback2.xml").addClasses( + TestResourceDiscoveryComponent.class, TestResourceComponent.class, PluginTwoCallback1.class, PluginTwoCallback2.class); + pluginJar2.withRequiredPluginsFrom(pluginJar1); + return pluginJar2; + } + + @ArquillianResource + private MockingServerServices serverServices; + + @ArquillianResource + private PluginContainerConfiguration pluginContainerConfiguration; + + @ArquillianResource + private PluginContainer pluginContainer; + + private FakeServerInventory fakeServerInventory; + + private FakeServerInventory.CompleteDiscoveryChecker discoveryCompleteChecker; + + @BeforeDiscovery + public void resetServerServices() throws Exception { + // Set up our fake server discovery ServerService, which will auto-import all Resources in reports it receives. + serverServices.resetMocks(); + fakeServerInventory = new FakeServerInventory(); + discoveryCompleteChecker = fakeServerInventory.createAsyncDiscoveryCompletionChecker(2); + when(serverServices.getDiscoveryServerService().mergeInventoryReport(any(InventoryReport.class))).then( + fakeServerInventory.mergeInventoryReport(InventoryStatus.COMMITTED)); + } + + @AfterDiscovery + public void waitForAsyncDiscoveries() throws Exception { + if (discoveryCompleteChecker != null) { + discoveryCompleteChecker.waitForDiscoveryComplete(10000); + } + } + + @RunDiscovery + @Test(groups = "pc.itest.discoverycallbacks", priority = 20) // setting the group is important! otherwise, other tests will fail + public void testDiscoveryCallbacks() throws Exception { + // make sure our inventory is as we expect it to be + validatePluginContainerInventory(); + } + + private void validatePluginContainerInventory() throws Exception { + System.out.println("Validating PC inventory..."); + + Resource platform = pluginContainer.getInventoryManager().getPlatform(); + Assert.assertNotNull(platform); + Assert.assertEquals(platform.getInventoryStatus(), InventoryStatus.COMMITTED); + + Set<Resource> servers = platform.getChildResources(); + Assert.assertNotNull(servers); + Assert.assertEquals(servers.size(), 2); + + for (Resource server : servers) { + if (server.getResourceType().getName().equals("TestServerOne")) { + Assert.assertEquals(server.getName(), "PluginTwoCallback1:name"); + Assert.assertEquals(server.getVersion(), "PluginTwoCallback1:1.0"); + Assert.assertEquals(server.getPluginConfiguration().getSimpleValue("TestServerOne.prop1"), "PluginOneCallback:prop1"); + Assert.assertEquals(server.getPluginConfiguration().getSimpleValue("TestServerOne.prop2"), "PluginTwoCallback1:prop2"); + } else if (server.getResourceType().getName().equals("TestServerTwo")) { + Assert.assertEquals(server.getName(), "PluginTwoCallback2:name"); + Assert.assertEquals(server.getVersion(), "PluginTwoCallback2:1.0"); + Assert.assertEquals(server.getPluginConfiguration().getSimpleValue("TestServerTwo.prop1"), null); + Assert.assertEquals(server.getPluginConfiguration().getSimpleValue("TestServerTwo.prop2"), "PluginTwoCallback2:prop2"); + } else { + assert false : "got a resource we were not expecting: " + server; + } + } + + System.out.println("PC inventory validated successfully!"); + } + +} diff --git a/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginOneCallback.java b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginOneCallback.java new file mode 100644 index 0000000..6b24987 --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginOneCallback.java @@ -0,0 +1,22 @@ +package org.rhq.core.pc.inventory.discoverycallback; + +import org.rhq.core.domain.configuration.PropertySimple; +import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails; +import org.rhq.core.pluginapi.inventory.ResourceDiscoveryCallback; + +import java.util.Set; + +public class PluginOneCallback implements ResourceDiscoveryCallback{ + @Override + public void discoveredResources(Set<DiscoveredResourceDetails> discoveredDetails) throws Exception { + // we know our test discovery component detects one singleton server, let's tweek the details. + // note that plugin 2 (the one that depends our plugin) will also have a callback and will + // have a chance to overwrite what we do here. Our test code will test that the child plugin + // callback in that plugin's callback (PluginTwoCallback1) can tweek what we do here. + DiscoveredResourceDetails details = discoveredDetails.iterator().next(); + details.setResourceName("This will be overwritten by plugin two's test callback, so this string doesn't matter"); + details.setResourceVersion("This will be overwritten by plugin two's test callback, so this string doesn't matter"); + details.getPluginConfiguration().put(new PropertySimple("TestServerOne.prop1", "PluginOneCallback:prop1")); + System.out.println("!!!!!!!!!!!!!!" + this.getClass().getName() + "==>" + discoveredDetails); + } +} diff --git a/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback1.java b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback1.java new file mode 100644 index 0000000..e346b5a --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback1.java @@ -0,0 +1,21 @@ +package org.rhq.core.pc.inventory.discoverycallback; + +import org.rhq.core.domain.configuration.PropertySimple; +import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails; +import org.rhq.core.pluginapi.inventory.ResourceDiscoveryCallback; + +import java.util.Set; + +public class PluginTwoCallback1 implements ResourceDiscoveryCallback{ + @Override + public void discoveredResources(Set<DiscoveredResourceDetails> discoveredDetails) throws Exception { + // the resource was discovered, and our plugin one's callback has tweeked these details, but now we can + // further tweek the details here + DiscoveredResourceDetails details = discoveredDetails.iterator().next(); + details.setResourceName("PluginTwoCallback1:name"); + details.setResourceVersion("PluginTwoCallback1:1.0"); + // notice we do not touch plugin config property TestServerOne.prop1, let the first plugin's callback touch it + details.getPluginConfiguration().put(new PropertySimple("TestServerOne.prop2", "PluginTwoCallback1:prop2")); + System.out.println("!!!!!!!!!!!!!!" + this.getClass().getName() + "==>" + discoveredDetails); + } +} diff --git a/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback2.java b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback2.java new file mode 100644 index 0000000..fdcab1c --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/java/org/rhq/core/pc/inventory/discoverycallback/PluginTwoCallback2.java @@ -0,0 +1,19 @@ +package org.rhq.core.pc.inventory.discoverycallback; + +import org.rhq.core.domain.configuration.PropertySimple; +import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails; +import org.rhq.core.pluginapi.inventory.ResourceDiscoveryCallback; + +import java.util.Set; + +public class PluginTwoCallback2 implements ResourceDiscoveryCallback{ + @Override + public void discoveredResources(Set<DiscoveredResourceDetails> discoveredDetails) throws Exception { + DiscoveredResourceDetails details = discoveredDetails.iterator().next(); + details.setResourceName("PluginTwoCallback2:name"); + details.setResourceVersion("PluginTwoCallback2:1.0"); + // notice we do not touch plugin config property TestServerTwo.prop1, our test code will check that it is null + details.getPluginConfiguration().put(new PropertySimple("TestServerTwo.prop2", "PluginTwoCallback2:prop2")); + System.out.println("!!!!!!!!!!!!!!" + this.getClass().getName() + "==>" + discoveredDetails); + } +} diff --git a/modules/core/plugin-container-itest/src/test/resources/discovery-callback1.xml b/modules/core/plugin-container-itest/src/test/resources/discovery-callback1.xml new file mode 100644 index 0000000..addfabd --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/resources/discovery-callback1.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<plugin name="DiscoveryCallbackPluginOne" + package="org.rhq.core.pc.inventory.testplugin" + version="1.0" + xmlns="urn:xmlns:rhq-plugin" + xmlns:c="urn:xmlns:rhq-configuration"> + + <discovery-callbacks> + <type-callback plugin="DiscoveryCallbackPluginOne" type="TestServerOne" callbackClass="org.rhq.core.pc.inventory.discoverycallback.PluginOneCallback"/> + </discovery-callbacks> + + <server name="TestServerOne" + discovery="TestResourceDiscoveryComponent" + class="TestResourceComponent"> + <plugin-configuration> + <c:simple-property name="TestServerOne.prop1"/> + <c:simple-property name="TestServerOne.prop2"/> + </plugin-configuration> + </server> +</plugin> diff --git a/modules/core/plugin-container-itest/src/test/resources/discovery-callback2.xml b/modules/core/plugin-container-itest/src/test/resources/discovery-callback2.xml new file mode 100644 index 0000000..fcf24ef --- /dev/null +++ b/modules/core/plugin-container-itest/src/test/resources/discovery-callback2.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<plugin name="DiscoveryCallbackPluginTwo" + package="org.rhq.core.pc.inventory.testplugin" + version="1.0" + xmlns="urn:xmlns:rhq-plugin" + xmlns:c="urn:xmlns:rhq-configuration"> + + <depends plugin="DiscoveryCallbackPluginOne" /> + + <discovery-callbacks> + <type-callback plugin="DiscoveryCallbackPluginOne" type="TestServerOne" callbackClass="org.rhq.core.pc.inventory.discoverycallback.PluginTwoCallback1" /> + <type-callback plugin="DiscoveryCallbackPluginTwo" type="TestServerTwo" callbackClass="org.rhq.core.pc.inventory.discoverycallback.PluginTwoCallback2" /> + </discovery-callbacks> + + <server name="TestServerTwo" + discovery="TestResourceDiscoveryComponent" + class="TestResourceComponent"> + <plugin-configuration> + <c:simple-property name="TestServerTwo.prop1" /> + <c:simple-property name="TestServerTwo.prop2" /> + </plugin-configuration> + </server> +</plugin> diff --git a/modules/core/plugin-container/src/main/java/org/rhq/core/pc/inventory/InventoryManager.java b/modules/core/plugin-container/src/main/java/org/rhq/core/pc/inventory/InventoryManager.java index 406d65d..f838af5 100644 --- a/modules/core/plugin-container/src/main/java/org/rhq/core/pc/inventory/InventoryManager.java +++ b/modules/core/plugin-container/src/main/java/org/rhq/core/pc/inventory/InventoryManager.java @@ -117,6 +117,7 @@ import org.rhq.core.pluginapi.inventory.ManualAddFacet; import org.rhq.core.pluginapi.inventory.ProcessScanResult; import org.rhq.core.pluginapi.inventory.ResourceComponent; import org.rhq.core.pluginapi.inventory.ResourceContext; +import org.rhq.core.pluginapi.inventory.ResourceDiscoveryCallback; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext; import org.rhq.core.pluginapi.operation.OperationContext; @@ -337,20 +338,58 @@ public class InventoryManager extends AgentService implements ContainerService,
long timeout = getDiscoveryComponentTimeout(context.getResourceType());
+ Set<DiscoveredResourceDetails> results; + try { ResourceDiscoveryComponent proxy = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy( context.getResourceType(), component, timeout, parentResourceContainer); - Set<DiscoveredResourceDetails> results = proxy.discoverResources(context); - return results; + results = proxy.discoverResources(context); } catch (TimeoutException te) { log.warn("Discovery for Resources of [" + context.getResourceType() + "] has been running for more than " + timeout + " milliseconds. This may be a plugin bug.", te); - return null; + results = null; } catch (BlacklistedException be) { // Discovery did not run, because the ResourceType was blacklisted during a prior discovery scan. log.debug(ThrowableUtil.getAllMessages(be)); - return null; + results = null; + } + + if (results == null || results.isEmpty()) { + return results; } + + // funnel the results through any discovery callbacks that are defined on the discovered resource type + Map<String, List<String>> callbacks = this.pluginManager.getMetadataManager().getDiscoveryCallbacks(context.getResourceType()); + if (callbacks == null || callbacks.isEmpty()) { + return results; // no callbacks defined, return the discovered details as-is + } + + ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); + int callbackCount = 0; + PluginComponentFactory pluginComponentFactory = PluginContainer.getInstance().getPluginComponentFactory(); + for (Map.Entry<String, List<String>> entry : callbacks.entrySet()) { + String pluginName = entry.getKey(); + List<String> callbackClassNames = entry.getValue(); + for (String className : callbackClassNames) { + ResourceDiscoveryCallback callback = pluginComponentFactory.getDiscoveryCallback(pluginName, className); + try { + Thread.currentThread().setContextClassLoader(callback.getClass().getClassLoader()); + // notice we call inline, in our calling thread - no time outs or anything; hopefully the plugin plays nice + callback.discoveredResources(results); + callbackCount++; + if (log.isDebugEnabled()) { + log.debug("Discovery callback [{" + pluginName + "}" + className + "] done. #invocations=" + callbackCount); + } + } catch (Throwable t) { + log.error("Discovery callback [{" + pluginName + "}" + className + "] failed with exception: " + + ThrowableUtil.getAllMessages(t), t); + } finally { + Thread.currentThread().setContextClassLoader(originalContextClassLoader); + } + } + } + + return results; }
/** diff --git a/modules/core/plugin-container/src/main/java/org/rhq/core/pc/plugin/PluginComponentFactory.java b/modules/core/plugin-container/src/main/java/org/rhq/core/pc/plugin/PluginComponentFactory.java index 1f3fcd7..9f0502c 100644 --- a/modules/core/plugin-container/src/main/java/org/rhq/core/pc/plugin/PluginComponentFactory.java +++ b/modules/core/plugin-container/src/main/java/org/rhq/core/pc/plugin/PluginComponentFactory.java @@ -43,6 +43,7 @@ import org.rhq.core.pc.inventory.ResourceContainer; import org.rhq.core.pluginapi.inventory.ClassLoaderFacet; import org.rhq.core.pluginapi.inventory.ResourceComponent; import org.rhq.core.pluginapi.inventory.ResourceContext; +import org.rhq.core.pluginapi.inventory.ResourceDiscoveryCallback; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent;
/** @@ -225,6 +226,29 @@ public class PluginComponentFactory implements ContainerService { } }
+ /** + * This will create a new {@link ResourceDiscoveryCallback} instance that can be used to process + * details of newly discovered resources. + * + * @return a new discovery callback loaded in the proper classloader + * @throws PluginContainerException if failed to create the discovery callback instance + */ + public ResourceDiscoveryCallback getDiscoveryCallback(String pluginName, String callbackClassName) + throws PluginContainerException { + + // same classloader as plugin discovery component would use - with null parent, its just the plugin classloader + ClassLoader classLoader = getDiscoveryComponentClassLoader(null, pluginName); + + ResourceDiscoveryCallback callback = (ResourceDiscoveryCallback) instantiateClass(classLoader, + callbackClassName); + + if (log.isDebugEnabled()) { + log.debug("Created discovery callback [" + callbackClassName + "] for plugin [" + pluginName + ']'); + } + + return callback; + } + private List<URL> askDiscoveryComponentForAdditionalClasspathUrls(Resource resource, ResourceContainer parentContainer) throws Throwable {
rhq-commits@lists.fedorahosted.org