/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.core.pc.inventory; import static org.rhq.core.util.StringUtil.isNotBlank; import gnu.trove.map.TIntObjectMap; import gnu.trove.map.hash.TIntObjectHashMap; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.rhq.core.clientapi.agent.PluginContainerException; import org.rhq.core.clientapi.agent.discovery.DiscoveryAgentService; import org.rhq.core.clientapi.agent.discovery.InvalidPluginConfigurationClientException; import org.rhq.core.clientapi.agent.metadata.PluginMetadataManager; import org.rhq.core.clientapi.agent.metadata.ResourceTypeNotEnabledException; import org.rhq.core.clientapi.agent.upgrade.ResourceUpgradeRequest; import org.rhq.core.clientapi.agent.upgrade.ResourceUpgradeResponse; import org.rhq.core.clientapi.descriptor.configuration.SimpleProperty; import org.rhq.core.clientapi.server.configuration.ConfigurationServerService; import org.rhq.core.clientapi.server.discovery.DiscoveryServerService; import org.rhq.core.clientapi.server.discovery.InvalidInventoryReportException; import org.rhq.core.clientapi.server.discovery.InventoryReport; import org.rhq.core.clientapi.server.discovery.StaleTypeException; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.ConfigurationUtility; import org.rhq.core.domain.configuration.Property; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.configuration.definition.ConfigurationDefinition; import org.rhq.core.domain.configuration.definition.PropertyDefinition; import org.rhq.core.domain.discovery.AvailabilityReport; import org.rhq.core.domain.discovery.MergeInventoryReportResults; import org.rhq.core.domain.discovery.MergeInventoryReportResults.ResourceTypeFlyweight; import org.rhq.core.domain.discovery.MergeResourceResponse; import org.rhq.core.domain.discovery.PlatformSyncInfo; import org.rhq.core.domain.discovery.ResourceSyncInfo; import org.rhq.core.domain.measurement.Availability; import org.rhq.core.domain.measurement.AvailabilityType; import org.rhq.core.domain.measurement.MeasurementScheduleRequest; import org.rhq.core.domain.measurement.ResourceMeasurementScheduleRequest; import org.rhq.core.domain.resource.Agent; import org.rhq.core.domain.resource.InventoryStatus; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceCategory; import org.rhq.core.domain.resource.ResourceCreationDataType; import org.rhq.core.domain.resource.ResourceError; import org.rhq.core.domain.resource.ResourceErrorType; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.domain.resource.ResourceUpgradeReport; import org.rhq.core.pc.ContainerService; import org.rhq.core.pc.PluginContainer; import org.rhq.core.pc.PluginContainerConfiguration; import org.rhq.core.pc.ServerServices; import org.rhq.core.pc.agent.AgentRegistrar; import org.rhq.core.pc.agent.AgentService; import org.rhq.core.pc.agent.AgentServiceStreamRemoter; import org.rhq.core.pc.availability.AvailabilityContextImpl; import org.rhq.core.pc.component.ComponentInvocationContextImpl; import org.rhq.core.pc.configuration.ConfigurationCheckExecutor; import org.rhq.core.pc.content.ContentContextImpl; import org.rhq.core.pc.content.ContentManager; import org.rhq.core.pc.drift.sync.DriftSyncManager; import org.rhq.core.pc.event.EventContextImpl; import org.rhq.core.pc.event.EventManager; import org.rhq.core.pc.inventory.ResourceContainer.ResourceComponentState; import org.rhq.core.pc.measurement.MeasurementManager; import org.rhq.core.pc.operation.OperationContextImpl; import org.rhq.core.pc.operation.OperationManager; import org.rhq.core.pc.plugin.BlacklistedException; import org.rhq.core.pc.plugin.CanonicalResourceKey; import org.rhq.core.pc.plugin.PluginComponentFactory; import org.rhq.core.pc.plugin.PluginManager; import org.rhq.core.pc.upgrade.DiscoverySuspendedException; import org.rhq.core.pc.upgrade.ResourceUpgradeDelegate; import org.rhq.core.pc.util.DiscoveryComponentProxyFactory; import org.rhq.core.pc.util.FacetLockType; import org.rhq.core.pc.util.LoggingThreadFactory; import org.rhq.core.pluginapi.availability.AvailabilityContext; import org.rhq.core.pluginapi.content.ContentContext; import org.rhq.core.pluginapi.event.EventContext; import org.rhq.core.pluginapi.inventory.ClassLoaderFacet; import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails; import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException; import org.rhq.core.pluginapi.inventory.InventoryContext; 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; import org.rhq.core.pluginapi.upgrade.ResourceUpgradeCallback; import org.rhq.core.pluginapi.upgrade.ResourceUpgradeContext; import org.rhq.core.pluginapi.upgrade.ResourceUpgradeFacet; import org.rhq.core.system.SystemInfo; import org.rhq.core.system.SystemInfoFactory; import org.rhq.core.util.StopWatch; import org.rhq.core.util.exception.ThrowableUtil; import org.rhq.core.util.exception.WrappedRemotingException; /** * Manages the process of both auto-detection of servers and runtime detection of services across all plugins. Manages * their scheduling and result sending as well as the general inventory model. * <p> * This is an Agent service; its DiscoveryAgentService interface is made remotely accessible if it is deployed within * the Agent. * </p> * * @author Greg Hinkle * @author Ian Springer * @author Jay Shaughnessy */ @SuppressWarnings({ "rawtypes", "unchecked" }) public class InventoryManager extends AgentService implements ContainerService, DiscoveryAgentService { private static final Log log = LogFactory.getLog(InventoryManager.class); private static final String INVENTORY_THREAD_POOL_NAME = "InventoryManager.discovery"; private static final String AVAIL_THREAD_POOL_NAME = "InventoryManager.availability"; private static final int AVAIL_THREAD_POOL_CORE_POOL_SIZE = 1; private static final int COMPONENT_START_TIMEOUT = 60 * 1000; // 60 seconds private static final int COMPONENT_STOP_TIMEOUT = 5 * 1000; // 5 seconds static private final int SYNC_BATCH_SIZE; static { int syncBatchSize = 500; try { syncBatchSize = Integer.parseInt(System.getProperty("rhq.agent.sync.batch.size", "500")); } catch (Throwable t) { // } SYNC_BATCH_SIZE = syncBatchSize; } private final PluginContainerConfiguration configuration; private ScheduledThreadPoolExecutor inventoryThreadPoolExecutor; private ScheduledThreadPoolExecutor availabilityThreadPoolExecutor; // The executors are Callable private final AutoDiscoveryExecutor serverScanExecutor; private final RuntimeDiscoveryExecutor serviceScanExecutor; private final AvailabilityExecutor availabilityExecutor; private final Agent agent; /** * Child scanning process. */ private volatile Future<?> serviceScan = null; /** * Root platform resource, required to be root of entire inventory tree in this agent */ private Resource platform; /** * if the {@link #getPlatform() platform} has inventory status of NEW, this indicates it was committed before but * was deleted recently */ private boolean newPlatformWasDeletedRecently = false; // value only is valid/relevant if platform.getInventoryStatus == NEW private final ReentrantReadWriteLock inventoryLock = new ReentrantReadWriteLock(true); /** * Used only for the outside the agent model to # resources */ private final AtomicInteger temporaryKeyIndex = new AtomicInteger(-1); /** * UUID to ResourceContainer map */ private final Map<String, ResourceContainer> resourceContainersByUUID = new ConcurrentHashMap<String, ResourceContainer>( 500); /** * ResourceID to ResourceContainer map */ private final TIntObjectMap<ResourceContainer> resourceContainerByResourceId = new TIntObjectHashMap<ResourceContainer>( 500); /** * Collection of event listeners to inform of changes to the inventory. */ private final Set<InventoryEventListener> inventoryEventListeners = new CopyOnWriteArraySet<InventoryEventListener>(); private final PluginManager pluginManager; private final DiscoveryComponentProxyFactory discoveryComponentProxyFactory; /** * Handles the resource upgrade during the initialization of the inventory manager. */ private final ResourceUpgradeDelegate resourceUpgradeDelegate = new ResourceUpgradeDelegate(this); private final PluginComponentFactory pluginFactory; private final EventManager eventManager; private final OperationManager operationManager; private final MeasurementManager measurementManager; private final ContentManager contentManager; /** * Constructs a new instance. * Call {@link #initialize()} once constructed. */ public InventoryManager(PluginContainerConfiguration configuration, AgentServiceStreamRemoter streamRemoter, PluginManager pluginManager) { super(DiscoveryAgentService.class, streamRemoter); this.configuration = configuration; if (pluginManager == null) { throw new NullPointerException("pluginManager is null"); } this.pluginManager = pluginManager; pluginFactory = new PluginComponentFactory(this, pluginManager); eventManager = new EventManager(configuration); operationManager = new OperationManager(configuration, getStreamRemoter()); measurementManager = new MeasurementManager(configuration, getStreamRemoter(), this); contentManager = new ContentManager(configuration, getStreamRemoter(), this); availabilityExecutor = new AvailabilityExecutor(this); serviceScanExecutor = new RuntimeDiscoveryExecutor(this, configuration); serverScanExecutor = new AutoDiscoveryExecutor(null, this); discoveryComponentProxyFactory = new DiscoveryComponentProxyFactory(pluginFactory); agent = new Agent(this.configuration.getContainerName(), null, 0, null, null); } /** * Loads the inventory, sets up the platform, measurement manager, and * upgrades the resources, and schedules periodic tasks. * * Because of dependency circularity, initialization has to happen * outside of the constructor. * * ClassLoaderManager depends on InventoryManager to return the platform resource. * InventoryManager calls PluginComponentFactory which then calls ClassLoaderManager to load classes. */ public void initialize() { inventoryLock.writeLock().lock(); try { log.info("Initializing Inventory Manager..."); discoveryComponentProxyFactory.initialize(); if (configuration.isInsideAgent()) { loadFromDisk(); } // Discover the platform first thing. executePlatformScan(); //try the resource upgrade before we have any schedulers set up //so that we don't get any interventions from concurrently running //discoveries. activateAndUpgradeResources(); // Never run more than one avail check at a time. availabilityThreadPoolExecutor = new ScheduledThreadPoolExecutor(AVAIL_THREAD_POOL_CORE_POOL_SIZE, new LoggingThreadFactory(AVAIL_THREAD_POOL_NAME, true)); // Never run more than one discovery scan at a time (service and service scans share the same pool). inventoryThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new LoggingThreadFactory( INVENTORY_THREAD_POOL_NAME, true)); // Only schedule periodic discovery scans and avail checks if we are running inside the RHQ Agent (versus // inside EmbJopr). if (configuration.isInsideAgent()) { // After an initial delay (5s by default), periodically run an availability check (every 1m by default). availabilityThreadPoolExecutor.scheduleWithFixedDelay(availabilityExecutor, configuration.getAvailabilityScanInitialDelay(), configuration.getAvailabilityScanPeriod(), TimeUnit.SECONDS); // After an initial delay (10s by default), periodically run a server discovery scan (every 15m by default). inventoryThreadPoolExecutor.scheduleWithFixedDelay(serverScanExecutor, configuration.getServerDiscoveryInitialDelay(), configuration.getServerDiscoveryPeriod(), TimeUnit.SECONDS); // After an initial delay (20s by default), periodically run a service discovery scan (every 1d by default). inventoryThreadPoolExecutor.scheduleWithFixedDelay(serviceScanExecutor, configuration.getServiceDiscoveryInitialDelay(), configuration.getServiceDiscoveryPeriod(), TimeUnit.SECONDS); } } finally { inventoryLock.writeLock().unlock(); } log.info("Inventory Manager initialized."); } /** * @see ContainerService#shutdown() */ @Override public void shutdown() { PluginContainer.shutdownExecutorService(this.inventoryThreadPoolExecutor, true); PluginContainer.shutdownExecutorService(this.availabilityThreadPoolExecutor, true); if (this.configuration.isInsideAgent()) { this.persistToDisk(); } this.discoveryComponentProxyFactory.shutdown(); this.inventoryEventListeners.clear(); this.resourceContainersByUUID.clear(); this.resourceContainerByResourceId.clear(); } /** * Invokes the given discovery component in order to discover resources. This will return * the discovered resources' details as returned by the discovery component. This may return * an empty set if nothing is discovered. This may return <code>null</code> if for some reason * we could not invoke the discovery component. * * @param parentResourceContainer the container of the resource under which we are going to execute the discovery * @param component the discovery component that will actually go out and discover resources * @param context the context for use by the discovery component * @return the details of all discovered resources, may be empty or <code>null</code> * * @throws DiscoverySuspendedException if the discovery is suspended due to a resource upgrade failure * @throws Exception if the discovery component threw an exception */ public Set<DiscoveredResourceDetails> invokeDiscoveryComponent(ResourceContainer parentResourceContainer, ResourceDiscoveryComponent component, ResourceDiscoveryContext context) throws Exception { Resource parentResource = parentResourceContainer == null ? null : parentResourceContainer.getResource(); if (resourceUpgradeDelegate.hasUpgradeFailedInChildren(parentResource, context.getResourceType())) { String message = "Discovery of [" + context.getResourceType() + "] has been suspended under " + (parentResource == null ? " the platform " : parentResource) + " because some of its siblings failed to upgrade."; log.debug(message); throw new DiscoverySuspendedException(message); } long timeout = getDiscoveryComponentTimeout(); Set<DiscoveredResourceDetails> results; try { ResourceDiscoveryComponent proxy = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy( context.getResourceType(), component, timeout, parentResourceContainer); 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); results = null; } catch (BlacklistedException be) { // Discovery did not run, because the ResourceType was blacklisted during a prior discovery scan. if (log.isDebugEnabled()) { log.debug(ThrowableUtil.getAllMessages(be)); } results = null; } if (results == null || results.isEmpty()) { return results; } // find the discovery callbacks defined, if there are none, just return the results as-is Map<String, List<String>> callbacks = this.pluginManager.getMetadataManager().getDiscoveryCallbacks( context.getResourceType()); if (callbacks == null || callbacks.isEmpty()) { return results; } // funnel the results through the discovery callbacks that are defined on the discovered resource type // we do them one discovered resource details as a time, giving each callback the opportunity to handle each ResourceDiscoveryCallback.DiscoveryCallbackResults callbackResults; PluginComponentFactory pluginComponentFactory = PluginContainer.getInstance().getPluginComponentFactory(); ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); for (Iterator<DiscoveredResourceDetails> detailsIterator = results.iterator(); detailsIterator.hasNext();) { DiscoveredResourceDetails details = detailsIterator.next(); int callbackCount = 0; boolean stopProcessing = false; // if true, a callback told us he found a details that he modified and we should stop boolean abortDiscovery = false; // if true, multiple callbacks claimed ownership which isn't allowed - its discovery will be aborted boolean vetoDiscovery = false; // if true, a callback veto'ed the resource - it should not be discovered at all 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()); callbackResults = callback.discoveredResources(details);// inline in our calling thread - no time outs or anything; hopefully the plugin plays nice callbackCount++; if (log.isDebugEnabled()) { log.debug("Discovery callback [{" + pluginName + "}" + className + "] returned [" + callbackResults + "] #invocations=" + callbackCount); } switch (callbackResults) { case PROCESSED: { if (stopProcessing) { abortDiscovery = true; log.warn("Another discovery callback [{" + pluginName + "}" + className + "] processed details [" + details + "]. This is not allowed. Discovery will be aborted for that resource"); } else { stopProcessing = true; } break; } case VETO: { vetoDiscovery = true; log.warn("Discovery callback [{" + pluginName + "}" + className + "] vetoed resource [" + details + "]. Discovery will be skipped for that resource and it will not be inventoried."); break; } default: { // callback left the details unprocessed, nothing to do. break; } } // note that we keep going, even if we set stopProcessing is true - this is because we // want to keep calling callbacks and check if they, too, think they can identify the details. If // they can, something is wrong and we want to abort since more than one callback should not be // claiming ownership of the same details. We could, in the future, not do this check and just // stop calling callbacks the first time a callback tells us it processed details } catch (Throwable t) { log.error("Discovery callback [{" + pluginName + "}" + className + "] failed", t); } finally { Thread.currentThread().setContextClassLoader(originalContextClassLoader); } } // for each callback } // for each plugin if (abortDiscovery) { // provide a backdoor escape hatch - you can avoid aborting by setting this sysprop to true if (!Boolean.getBoolean("rhq.agent.discovery-callbacks.never-abort")) { detailsIterator.remove(); // we do not want to process this details } } if (vetoDiscovery) { detailsIterator.remove(); } } return results; } /** * Invokes the given discovery component in order to manually add a Resource with the specified plugin config. This * will return the discovered resource's detail as returned by the discovery component, or it may return * an empty set if nothing is discovered. This may return <code>null</code> if for some reason * we could not invoke the discovery component. * * * @param component the discovery component that will actually go out and discover resources * @param pluginConfig the plugin configuration to be used to connect to the resource to be discovered * @param context the context for use by the discovery component * @param parentResourceContainer * @return the details of all discovered resources, may be empty or <code>null</code> * * @throws Exception if the discovery component threw an exception */ private DiscoveredResourceDetails discoverResource(ResourceDiscoveryComponent component, Configuration pluginConfig, ResourceDiscoveryContext context, ResourceContainer parentResourceContainer) throws Exception { long timeout = getDiscoveryComponentTimeout(); try { ManualAddFacet proxy = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy( context.getResourceType(), component, timeout, ManualAddFacet.class, parentResourceContainer); DiscoveredResourceDetails result = proxy.discoverResource(pluginConfig, context); return result; } catch (TimeoutException te) { log.warn("Manual add of Resource of type [" + context.getResourceType() + "] with plugin configuration [" + pluginConfig.toString(true) + "] has been running for more than " + timeout + " milliseconds. This may be a plugin bug.", te); return null; } catch (BlacklistedException be) { if (log.isDebugEnabled()) { log.debug(ThrowableUtil.getAllMessages(be)); } return null; } } /** * Invokes the given discovery component's ClassLoaderFacet in order to obtain * additional jars for the resource's classloader. This will return * the discovered resources' details as returned. * * @param resource the resource whose component is to be invoked * @param component the discovery component that will actually go out and discover resources * @param parentContainer the activated parent container * @return the additional jars for the resource's classloader * * @throws Throwable if the discovery component threw an exception */ public List<URL> invokeDiscoveryComponentClassLoaderFacet(Resource resource, ResourceDiscoveryComponent component, ResourceContainer parentContainer) throws Throwable { ResourceComponent parentComponent = parentContainer.getResourceComponent(); ResourceContext parentResourceContext = parentContainer.getResourceContext(); ResourceType resourceType = resource.getResourceType(); long timeout = getDiscoveryComponentTimeout(); ClassLoaderFacet proxy = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy(resourceType, component, timeout, ClassLoaderFacet.class, parentContainer); ResourceDiscoveryContext discoveryContext = new ResourceDiscoveryContext(resourceType, parentComponent, parentResourceContext, SystemInfoFactory.createSystemInfo(), null, null, this.configuration.getContainerName(), this.configuration.getPluginContainerDeployment()); // Configurations are not immutable, so clone the plugin config, so the plugin will not be able to change the // actual PC-managed plugin config. Configuration pluginConfigClone = resource.getPluginConfiguration().deepCopy(false); // TODO (ips): Clone the ResourceType too for the same reason. DiscoveredResourceDetails details = new DiscoveredResourceDetails(resourceType, resource.getResourceKey(), resource.getName(), resource.getVersion(), resource.getDescription(), pluginConfigClone, null); // TODO: I have a feeling we'll need process info, how to get it?? List<URL> results = proxy.getAdditionalClasspathUrls(discoveryContext, details); return results; } public <T extends ResourceComponent<?>> ResourceUpgradeReport invokeDiscoveryComponentResourceUpgradeFacet( ResourceType resourceType, ResourceDiscoveryComponent<T> component, ResourceUpgradeContext<T> inventoriedResource, ResourceContainer parentResourceContainer) throws Throwable { long timeout = getDiscoveryComponentTimeout(); try { ResourceUpgradeFacet<T> proxy = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy( resourceType, component, timeout, ResourceUpgradeFacet.class, parentResourceContainer); ResourceUpgradeReport report = proxy.upgrade(inventoriedResource); //funnel the report through all the optional resource upgrade callbacks Map<String, List<String>> callbacks = this.pluginManager.getMetadataManager() .getResourceUpgradeCallbacks(resourceType); if (callbacks != null) { //never pass a nul report to the callbacks... if it is null at the moment, just //create a new "blank" one. The upgrade delegate will see that there's nothing to upgrade //in it. if (report == null) { report = new ResourceUpgradeReport(); } //no timeouts or anything, just direct invocation.. let's hope plugins play nice for (Map.Entry<String, List<String>> e : callbacks.entrySet()) { String pluginName = e.getKey(); List<String> callbackClasses = e.getValue(); for (String cls : callbackClasses) { ResourceUpgradeCallback<T> callback = getPluginComponentFactory() .getResourceUpgradeCallback(pluginName, cls); try { callback.upgrade(report, inventoriedResource); } catch (Throwable t) { log.error("Resource upgrade callback [" + cls + "] from plugin [" + pluginName + "] failed" + " on resource " + inventoriedResource.getResourceDetails()); } } } } return report; } catch (BlacklistedException e) { log.debug(e); return null; } } public DiscoveryComponentProxyFactory getDiscoveryComponentProxyFactory() { return this.discoveryComponentProxyFactory; } private long getDiscoveryComponentTimeout() { // TODO: remove this system property and put the timeout in the plugin descriptor; // use the type to find what timeout to use since each type can have metadata determining the timeout // default should be 300000. long timeout = Long.parseLong(System.getProperty("rhq.test.discovery-timeout", "300000")); return timeout; } @Nullable public ResourceContainer getResourceContainer(String uuid) { return this.resourceContainersByUUID.get(uuid); } @Nullable public ResourceContainer getResourceContainer(CanonicalResourceKey canonicalId) { ResourceContainer resourceContainer = null; for (Map.Entry<String, ResourceContainer> entry : resourceContainersByUUID.entrySet()) { ResourceContainer container = entry.getValue(); Resource resource = container.getResource(); if (resource != null) { Resource parent = resource.getParentResource(); if (parent != null) { try { CanonicalResourceKey currentCanonicalId = new CanonicalResourceKey(resource, parent); if (currentCanonicalId.equals(canonicalId)) { resourceContainer = container; break; } } catch (PluginContainerException ignore) { // TODO not sure what to do here, when would this ever happen? for now, ignore } } } } return resourceContainer; } @Nullable public ResourceContainer getResourceContainer(Resource resource) { String uuid = resource.getUuid(); if (uuid == null) return null; return this.resourceContainersByUUID.get(uuid); } @Nullable public ResourceContainer getResourceContainer(int resourceId) { if (resourceId == 0) { // I've already found one place where passing in 0 was very bad - I want to be very noisy in the log // when this happens but not throw an exception, for fear I might break something. // I'll just return null instead; hopefully, callers are checking for null appropriately. log.warn("Cannot get a resource container for an invalid resource ID=" + resourceId); if (log.isDebugEnabled()) { //noinspection ThrowableInstanceNeverThrown log.debug("Stack trace follows:", new Throwable("This is where resource ID=[" + resourceId + "] was passed in")); } return null; } ResourceContainer resourceContainer = this.resourceContainerByResourceId.get(resourceId); if (resourceContainer != null) { return resourceContainer; } // We did not find the UUID in above map. // Check on the classical way if the container is present and populate the // resourceId -> resourceContainer map for the next call into this method. ResourceContainer retContainer = null; for (ResourceContainer container : resourceContainersByUUID.values()) { if (resourceId == container.getResource().getId()) { retContainer = container; this.resourceContainerByResourceId.put(resourceId, retContainer); break; } } return retContainer; } void executePlatformScan() { log.debug("Executing platform scan..."); Resource discoveredPlatform = discoverPlatform(); try { mergeResourceFromDiscovery(discoveredPlatform, null); } catch (PluginContainerException e) { throw new IllegalStateException(e); } } @Override public void updatePluginConfiguration(int resourceId, Configuration newPluginConfiguration) throws InvalidPluginConfigurationClientException, PluginContainerException { ResourceContainer container = getResourceContainer(resourceId); if (container == null) { throw new PluginContainerException("Cannot update plugin configuration for unknown Resource with id [" + resourceId + "]"); } Resource resource = container.getResource(); // First stop the resource component. deactivateResource(resource); // Then update the resource's plugin config. resource.setPluginConfiguration(newPluginConfiguration); // And finally restart the resource component. try { activateResource(resource, container, true); // TODO: What about re-activating the Resource's descendants? } catch (InvalidPluginConfigurationException e) { String errorMessage = "Unable to connect to managed resource of type [" + resource.getResourceType().getName() + "] using the specified connection properties."; log.info(errorMessage, e); errorMessage += ((e.getLocalizedMessage() != null) ? (" " + e.getLocalizedMessage()) : ""); // In the exception we throw over to the server, strip the InvalidPluginConfigurationException out of the // stack trace, but append the message from that exception to the message of the exception we throw. This // will make for a nicer error message for the server to display in the UI. throw new InvalidPluginConfigurationClientException(errorMessage, (e.getCause() != null) ? new WrappedRemotingException(e.getCause()) : null); } } private InventoryReport submit(Callable<InventoryReport> c) { try { return inventoryThreadPoolExecutor.submit(c).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Scan execution was interrupted"); } catch (ExecutionException e) { // Should never happen, reports are always generated, even if they're just to report the error throw new RuntimeException("Unexpected exception", e); } } @Override @NotNull public InventoryReport executeServerScanImmediately() { return submit(serverScanExecutor); } @Override @NotNull public InventoryReport executeServiceScanImmediately() { return submit(serviceScanExecutor); } @NotNull public InventoryReport executeServiceScanImmediately(Resource resource) { RuntimeDiscoveryExecutor discoveryExecutor = new RuntimeDiscoveryExecutor(this, this.configuration, resource); return submit(discoveryExecutor); } @Override public boolean executeServiceScanDeferred() { // if there is a scan in progress, cancel it and start over. No need to bog down the agent with multiple // concurrent service scans. The one in progress is now dated, because we likely have just added agent-side // resources. if ((null != serviceScan) && !serviceScan.isDone()) { boolean isCanceled = serviceScan.cancel(true); if (isCanceled) { log.info("Canceled in-progress service scan in favor of starting new scan. Partial scan will still be processed."); } } serviceScan = this.inventoryThreadPoolExecutor.schedule((Runnable) this.serviceScanExecutor, configuration.getChildResourceDiscoveryDelay(), TimeUnit.SECONDS); return true; } @Override public void executeServiceScanDeferred(int resourceId) { executeServiceScanDeferred(resourceId, 0); } public void executeServiceScanDeferred(int resourceId, long scanDelayInMillis) { Resource resource = getResourceContainer(resourceId).getResource(); RuntimeDiscoveryExecutor discoveryExecutor = new RuntimeDiscoveryExecutor(this, this.configuration, resource); inventoryThreadPoolExecutor.schedule((Runnable) discoveryExecutor, scanDelayInMillis, TimeUnit.MILLISECONDS); } /** * This method implicitly calls {@link #handleReport(AvailabilityReport)} so any report generating entries * *will be sent to the server*. Callers should subsequently *NOT* send the report. * * @param changedOnlyReport * @return The report, for inspection */ @Override public AvailabilityReport executeAvailabilityScanImmediately(boolean changedOnlyReport) { return executeAvailabilityScanImmediately(changedOnlyReport, false); } /** * This method implicitly calls {@link #handleReport(AvailabilityReport)} so any report generating entries * *will be sent to the server*. Callers should subsequently *NOT* send the report. * * @param changedOnlyReport * @param forceChecks * @return The report, for inspection */ public AvailabilityReport executeAvailabilityScanImmediately(boolean changedOnlyReport, boolean forceChecks) { try { AvailabilityExecutor availExec = (forceChecks) ? new ForceAvailabilityExecutor(this) : new AvailabilityExecutor(this); if (changedOnlyReport) { availExec.sendChangesOnlyReportNextTime(); } else { availExec.sendFullReportNextTime(); } AvailabilityReport availabilityReport = availabilityThreadPoolExecutor.submit( (Callable<AvailabilityReport>) availExec).get(); // make sure the server is notified of any changes in availability handleReport(availabilityReport); return availabilityReport; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Availability scan execution was interrupted", e); } catch (ExecutionException e) { // Should never happen, reports are always generated, even if they're just to report the error throw new RuntimeException("Unexpected exception", e); } } @Override @NotNull public AvailabilityReport getCurrentAvailability(Resource resource, boolean changesOnly) { try { //make sure we have the full version of the resource ResourceContainer container = getResourceContainer(resource.getId()); if (container == null) { //don't bother doing anything return new AvailabilityReport(changesOnly, getAgent().getName()); } resource = container.getResource(); MeasurementScheduleRequest availSchedule = container.getAvailabilitySchedule(); boolean forceScanForRoot = availSchedule == null || availSchedule.isEnabled(); // force scan for root resource only if availability schedule is enabled AvailabilityExecutor availExec = new CustomScanRootAvailabilityExecutor(this, resource, forceScanForRoot); if (changesOnly) { availExec.sendChangesOnlyReportNextTime(); } else { availExec.sendFullReportNextTime(); } return availabilityThreadPoolExecutor.submit((Callable<AvailabilityReport>) availExec).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Availability scan execution was interrupted", e); } catch (ExecutionException e) { // Should never happen, reports are always generated, even if they're just to report the error throw new RuntimeException("Unexpected exception", e); } } /** * Same as calling {@link #requestAvailabilityCheck(Resource, boolean)} as: * <p/> * * <code>requestAvailabilityCheck(resource, false);</code> */ public void requestAvailabilityCheck(Resource resource) { requestAvailabilityCheck(resource, false); } /** * Request an ASAP avail check for the provided resource. If recursive, do the same for the children. * * @param resource * @param recursive */ public void requestAvailabilityCheck(Resource resource, boolean recursive) { if (null == resource) { return; } ResourceContainer resourceContainer = getResourceContainer(resource); if (null != resourceContainer) { // set the avail schedule time to 1 to ensure the resource will have an avail check performed on // the next availability scan. (note, do not set to 0, that has special semantics that will actually // push the avail check out one cycle. resourceContainer.setAvailabilityScheduleTime(1L); } if (recursive && null != resource.getChildResources()) { for (Resource child : resource.getChildResources()) { requestAvailabilityCheck(child, true); } } } public void setResourceEnablement(int resourceId, boolean setEnabled) { configuration.getServerServices().getDiscoveryServerService().setResourceEnablement(resourceId, setEnabled); } @Override public MergeResourceResponse manuallyAddResource(ResourceType resourceType, int parentResourceId, Configuration pluginConfiguration, int ownerSubjectId) throws InvalidPluginConfigurationClientException, PluginContainerException { // TODO (ghinkle): This is hugely flawed. It assumes discovery components will only return the manually discovered // resource, but never says this is required. It then proceeds to auto-import the first resource returned. For // discoveries that are process based it works because this passes in a null process scan... also a bad idea. // Look up the full Resource type (the one provided by the Server is just the keys). String resourceTypeString = resourceType.toString(); resourceType = this.pluginManager.getMetadataManager().getType(resourceType); if (resourceType == null) { throw new IllegalStateException("Server specified unknown Resource type: " + resourceTypeString); } MergeResourceResponse mergeResourceResponse; Resource resource = null; boolean resourceAlreadyExisted = false; try { ResourceContainer parentResourceContainer = getResourceContainer(parentResourceId); ResourceComponent parentResourceComponent = parentResourceContainer.getResourceComponent(); // Get the discovery component responsible for discovering resources of the specified resource type. PluginComponentFactory pluginComponentFactory = PluginContainer.getInstance().getPluginComponentFactory(); ResourceDiscoveryComponent discoveryComponent = pluginComponentFactory.getDiscoveryComponent(resourceType, parentResourceContainer); DiscoveredResourceDetails discoveredResourceDetails; if (discoveryComponent instanceof ManualAddFacet) { // The plugin is using the new manual add API. ResourceDiscoveryContext<ResourceComponent<?>> discoveryContext = new ResourceDiscoveryContext<ResourceComponent<?>>( resourceType, parentResourceComponent, parentResourceContainer.getResourceContext(), SystemInfoFactory.createSystemInfo(), new ArrayList<ProcessScanResult>(0), new ArrayList<Configuration>(0), this.configuration.getContainerName(), this.configuration.getPluginContainerDeployment()); // Ask the plugin's discovery component to find the new resource, throwing exceptions if it cannot be // found at all. discoveredResourceDetails = discoverResource(discoveryComponent, pluginConfiguration, discoveryContext, parentResourceContainer); if (discoveredResourceDetails == null) { log.info("Plugin Error: During manual add, discovery component method [" + discoveryComponent.getClass().getName() + ".discoverResource()] returned null " + "(either the Resource type was blacklisted or the plugin developer " + "did not implement support for manually discovered Resources correctly)."); throw new PluginContainerException("The [" + resourceType.getPlugin() + "] plugin does not properly support manual addition of [" + resourceType.getName() + "] Resources."); } } else { // The plugin is using the old manual add API, which we must continue to support to maintain // backward compatibility. log.info("Plugin Warning: Resource type '" + resourceType.getName() + "' from '" + resourceType.getPlugin() + "' is still using the deprecated manual Resource add API, " + "rather than the new ManualAddFacet interface."); List<Configuration> pluginConfigurations = new ArrayList<Configuration>(1); pluginConfigurations.add(pluginConfiguration); ResourceDiscoveryContext<ResourceComponent<?>> discoveryContext = new ResourceDiscoveryContext<ResourceComponent<?>>( resourceType, parentResourceComponent, parentResourceContainer.getResourceContext(), SystemInfoFactory.createSystemInfo(), new ArrayList<ProcessScanResult>(0), pluginConfigurations, this.configuration.getContainerName(), this.configuration.getPluginContainerDeployment()); // Ask the plugin's discovery component to find the new resource, throwing exceptions if it cannot be // found at all. try { Set<DiscoveredResourceDetails> discoveredResources = invokeDiscoveryComponent( parentResourceContainer, discoveryComponent, discoveryContext); if ((discoveredResources == null) || discoveredResources.isEmpty()) { log.info("Plugin Error: During manual add, discovery component method [" + discoveryComponent.getClass().getName() + ".discoverResources()] returned " + discoveredResources + " when passed a single plugin configuration " + "(either the resource type was blacklisted or the plugin developer " + "did not implement support for manually discovered resources correctly)."); throw new PluginContainerException("The [" + resourceType.getPlugin() + "] plugin does not properly support manual addition of [" + resourceType.getName() + "] resources."); } discoveredResourceDetails = discoveredResources.iterator().next(); } catch (DiscoverySuspendedException e) { String message = "The discovery class [" + discoveryComponent.getClass().getName() + "]" + " uses a legacy implementation of \"manual add\" functionality. Some of the child resources" + " with the resource type [" + resourceType + "] under the parent resource [" + parentResourceContainer.getResource() + "]" + " failed to upgrade, which makes it impossible to support the legacy manual-add implementation. Either upgrade the plugin [" + resourceType.getPlugin() + "] to successfully upgrade all resources or consider implementing the ManualAdd facet."; log.info(message); throw new PluginContainerException(message, e); } } // Create the new Resource and add it to inventory if it isn't already there. resource = createNewResource(discoveredResourceDetails); Resource parentResource = getResourceContainer(parentResourceId).getResource(); Resource existingResource = findMatchingChildResource(resource, parentResource); if (existingResource != null) { if (log.isDebugEnabled()) { log.debug("Manual add for resource type [" + resourceType.getName() + "] and parent resource id [" + parentResourceId + "] found a resource that already exists in inventory - updating existing resource [" + existingResource + "]"); } resourceAlreadyExisted = true; resource = existingResource; if (resource.getInventoryStatus() != InventoryStatus.COMMITTED) { resource.setPluginConfiguration(pluginConfiguration); } } else { if (log.isDebugEnabled()) { log.debug("Adding manually discovered resource [" + resource + "] to inventory..."); } resource.setInventoryStatus(InventoryStatus.COMMITTED); parentResource.addChildResourceWithoutAncestry(resource); initResourceContainer(resource); } // Make sure the resource's component is activated (i.e. started). boolean newPluginConfig = true; ResourceContainer resourceContainer = getResourceContainer(resource); if (log.isDebugEnabled()) { log.debug("Activating resource [" + resource + "]..."); } // NOTE: We don't mess with inventory status - that's the server's responsibility. // Tell the server to merge the resource into its inventory. DiscoveryServerService discoveryServerService = this.configuration.getServerServices() .getDiscoveryServerService(); mergeResourceResponse = discoveryServerService.addResource(resource, ownerSubjectId); // Sync our local resource up with the one now in server inventory. Treat this like a newlyCommittedResource // - set mtime (same as ctime for a new resource) to ensure this does not get picked up in an inventory sync // pass, we know we're currently in sync with the server. resource.setId(mergeResourceResponse.getResourceId()); resource.setMtime(mergeResourceResponse.getMtime()); Set newResources = new LinkedHashSet<Resource>(); newResources.add(resource); postProcessNewlyCommittedResources(newResources); performServiceScan(resource.getId()); // Note that it is important to activate the resource *AFTER* it has been synced with the // server so that the resource has a valid id (which is needed by at least the content // subsystem). try { activateResource(resource, resourceContainer, newPluginConfig); } catch (Throwable t) { // if it fails to start keep going, we already have the resource in inventory and // we are in sync with the server. The new resource will be unavailable but at least // it will be accessible and editable by the user. Report the start exception at the end. handleInvalidPluginConfigurationResourceError(resource, t); throw new PluginContainerException("The resource [" + resource + "] has been added but could not be started. Verify the supplied configuration values: ", t); } } // Catch any other RuntimeExceptions or Errors, so the server doesn't have to worry about deserializing or // catching them. Before rethrowing, wrap them in a WrappedRemotingException and then wrap that in either an // InvalidPluginConfigurationException or a PluginContainerException. catch (Throwable t) { if ((resource != null) && !resourceAlreadyExisted && (getResourceContainer(resource) != null)) { // If the resource got added to inventory, roll it back (i.e. deactivate it, then remove it from inventory). if (log.isDebugEnabled()) { log.debug("Rolling back manual add of resource of type [" + resourceType.getName() + "] - removing resource with id [" + resource.getId() + "] from inventory..."); } deactivateResource(resource); uninventoryResource(resource.getId()); } if (t instanceof InvalidPluginConfigurationException) { String errorMessage = "Unable to connect to managed resource of type [" + resourceType.getName() + "] using the specified connection properties - resource will not be added to inventory."; log.info(errorMessage, t); // In the exception we throw over to the server, strip the InvalidPluginConfigurationException out of the // stack trace, but append the message from that exception to the message of the exception we throw. This // will make for a nicer error message for the server to display in the UI. errorMessage += ((t.getLocalizedMessage() != null) ? (" " + t.getLocalizedMessage()) : ""); throw new InvalidPluginConfigurationClientException(errorMessage, (t.getCause() != null) ? new WrappedRemotingException(t.getCause()) : null); } else { log.error("Manual add failed for resource of type [" + resourceType.getName() + "] and parent resource id [" + parentResourceId + "]", t); throw new PluginContainerException("Failed to add resource with type [" + resourceType.getName() + "] and parent resource id [" + parentResourceId + "]", new WrappedRemotingException(t)); } } return mergeResourceResponse; } static Resource createNewResource(DiscoveredResourceDetails details) { // Use a CopyOnWriteArraySet for childResources to allow the field to be concurrently accessed safely // (i.e. to avoid ConcurrentModificationExceptions). Set<Resource> childResources = new CopyOnWriteArraySet<Resource>(); Resource resource = new Resource(childResources); resource.setUuid(UUID.randomUUID().toString()); resource.setResourceKey(details.getResourceKey()); resource.setName(details.getResourceName()); resource.setVersion(details.getResourceVersion()); resource.setDescription(details.getResourceDescription()); resource.setResourceType(details.getResourceType()); Configuration pluginConfiguration = details.getPluginConfiguration(); ConfigurationUtility.normalizeConfiguration(details.getPluginConfiguration(), details.getResourceType() .getPluginConfigurationDefinition(), false, false); resource.setPluginConfiguration(pluginConfiguration); return resource; } private Resource cloneResourceWithoutChildren(Resource resourceFromServer) { // Use a CopyOnWriteArraySet for childResources to allow the field to be concurrently accessed safely // (i.e. to avoid ConcurrentModificationExceptions). Set<Resource> childResources = new CopyOnWriteArraySet<Resource>(); Resource resource = new Resource(childResources); resource.setId(resourceFromServer.getId()); resource.setUuid(resourceFromServer.getUuid()); resource.setResourceKey(resourceFromServer.getResourceKey()); resource.setResourceType(resourceFromServer.getResourceType()); resource.setMtime(resourceFromServer.getMtime()); resource.setInventoryStatus(resourceFromServer.getInventoryStatus()); resource.setPluginConfiguration(resourceFromServer.getPluginConfiguration()); resource.setVersion(resourceFromServer.getVersion()); resource.setName(resourceFromServer.getName()); compactResource(resource); return resource; } /** * Returns the known availability for the resource. If the availability is not known, <code>null</code> is returned. * * @param resource the resource whose availability should be returned * * @return resource availability or <code>null</code> if not known */ @Nullable public Availability getAvailabilityIfKnown(Resource resource) { ResourceContainer resourceContainer = getResourceContainer(resource); if (resourceContainer != null) { if (ResourceComponentState.STARTED == resourceContainer.getResourceComponentState()) { Availability availability = resourceContainer.getAvailability(); return availability; } } return null; } public void handleReport(AvailabilityReport report) { // a null report means a non-committed inventory - we are either brand new or our platform was deleted recently if (report == null) { if ((this.platform != null) && (this.platform.getInventoryStatus() == InventoryStatus.NEW) && newPlatformWasDeletedRecently) { // let's make sure we are registered; its probable that our platform was deleted and we need to re-register log.info("No committed resources to send in our availability report - the platform/agent was deleted, let's re-register again"); registerWithServer(); newPlatformWasDeletedRecently = false; // we've tried to recover from our platform being deleted, let's not do it again } return; } List<AvailabilityReport.Datum> reportAvails = report.getResourceAvailability(); if (configuration.isInsideAgent() && reportAvails.size() > 0) { // Due to the asynchronous nature of the availability collection, // it is possible we may have collected availability of a resource that has just recently been deleted; // therefore, as a secondary check, let's remove any availabilities for resources that no longer exist. // I suppose after we do this check and before we send the report to the server that a resource could // then be deleted, but that time period where that could happen is now very small and thus this will // be a rare event. And even if that does happen, nothing catastrophic would happen on the server, // the report would be accepted normally, a message would be inserted in the server log indicating an empty // report was received, and the rest of the handling would be short-circuited. this.inventoryLock.readLock().lock(); try { AvailabilityReport.Datum[] avails = reportAvails.toArray(new AvailabilityReport.Datum[reportAvails .size()]); for (AvailabilityReport.Datum avail : avails) { int resourceId = avail.getResourceId(); ResourceContainer container = getResourceContainer(resourceId); if (container == null) { reportAvails.remove(avail); } } } finally { this.inventoryLock.readLock().unlock(); } if (reportAvails.size() > 0) { try { log.info("Sending availability report to Server..."); if (log.isDebugEnabled()) { log.debug("Availability report content: " + report.toString(log.isTraceEnabled())); } boolean ok = configuration.getServerServices().getDiscoveryServerService() .mergeAvailabilityReport(report); if (!ok) { // I guess I could immediately call executeAvailabilityScanImmediately and pass its results to // mergeAvailabilityReport again right now, but what happens if we've queued up a bunch of // changed-only reports and the server is out of sync - each time the server processes those // reports, we'd do an extra round trip with a full report (which will get very expensive). // Let's just flag our executor for the next time it runs to send a full report; this way // if we've got 100 queued changed-only reports, let the server fully process them and only // at the next time we run the avail scan will we send it a full report. It might make the // server sync up a little slower than we'd like, but it avoids a potential hammering of the // server with tons of full reports when that would be unnecessary. availabilityExecutor.sendFullReportNextTime(); } } catch (Exception e) { log.warn("Could not transmit availability report to server", e); availabilityExecutor.sendFullReportNextTime(); // just in case the agent and server are out of sync } } } } /** * Send an inventory report to the Server. * * @param report the inventory report to be sent * @return true if sending the report to the Server succeeded, or false otherwise */ public boolean handleReport(InventoryReport report) { if (!configuration.isInsideAgent()) { return true; } PlatformSyncInfo platformSyncInfo; Collection<ResourceTypeFlyweight> ignoredTypes; try { String reportType = (report.isRuntimeReport()) ? "runtime" : "server"; log.info("Sending [" + reportType + "] inventory report to Server..."); long startTime = System.currentTimeMillis(); DiscoveryServerService discoveryServerService = configuration.getServerServices() .getDiscoveryServerService(); MergeInventoryReportResults results = discoveryServerService.mergeInventoryReport(report); if (results != null) { platformSyncInfo = results.getPlatformSyncInfo(); ignoredTypes = results.getIgnoredResourceTypes(); } else { platformSyncInfo = null; ignoredTypes = null; } if (log.isDebugEnabled()) { log.debug(String.format("Server DONE merging inventory report [%d] ms.", (System.currentTimeMillis() - startTime))); } } catch (StaleTypeException e) { log.error("Failed to merge inventory report with server. The report contains one or more resource types " + "that have been marked for deletion. Notifying the plugin container that a reboot is needed to purge " + "stale types."); PluginContainer.getInstance().notifyRebootRequestListener(); return false; } catch (InvalidInventoryReportException e) { log.error("Failure sending inventory report to Server - was this Agent's platform deleted?", e); if ((this.platform != null) && (this.platform.getInventoryStatus() == InventoryStatus.NEW) && newPlatformWasDeletedRecently) { // let's make sure we are registered; its probable that our platform was deleted and we need to re-register log.info("The inventory report was invalid probably because the platform/Agent was deleted; let's re-register..."); registerWithServer(); newPlatformWasDeletedRecently = false; // we've tried to recover from our platform being deleted, let's not do it again } return false; } // tell our metadata manager what resource types are to be ignored PluginMetadataManager metadataMgr = this.pluginManager.getMetadataManager(); if (ignoredTypes != null && !ignoredTypes.isEmpty()) { Collection<ResourceType> ignoredSet = new HashSet<ResourceType>(ignoredTypes.size()); for (ResourceTypeFlyweight ignoredType : ignoredTypes) { ignoredSet.add(new ResourceType(ignoredType.getName(), ignoredType.getPlugin(), null, null)); } metadataMgr.setIgnoredResourceTypes(ignoredSet); } else { metadataMgr.setIgnoredResourceTypes(null); } //sync info can be null if the server hasn't received a full inventory report //from us yet. This can happen if this method is being invoked from inside the //resource upgrade executor to sync up with the server side inventory *JUST AFTER* //this agent registered with the server for the very first time. In that case //the server hasn't received any info from us yet. //Another (rare) scenario where this would happen would be when the platform resource type //would change. //In either case, let's sync up with the server - if it's got nothing, neither should the agent. if (platformSyncInfo != null) { syncPlatform(platformSyncInfo); } else { purgeObsoleteResources(Collections.<String> emptySet()); //can't live without a platform, but we just deleted it. Let's rediscover it. discoverPlatform(); } return true; } /** * Performs a full platform sync so that resources passed in are reflected in the agent's inventory. * * @param platformSyncInfo sync info on the platform and references to the top level servers. not null. */ private void syncPlatform(PlatformSyncInfo platformSyncInfo) { final Set<String> allServerSideUuids = new HashSet<String>(); ResourceSyncInfo platformResourceSyncInfo = platformSyncInfo.getPlatform(); // sync the platform because it does not get included in the top level server sync allServerSideUuids.add(platformResourceSyncInfo.getUuid()); // sync the top level service hierarchy addAllUuids(platformSyncInfo.getServices(), allServerSideUuids); // Add the platform sync info to the service hierarchy in order to process in one batch Collection<ResourceSyncInfo> syncInfos = platformSyncInfo.getServices(); syncInfos.add(platformResourceSyncInfo); log.info("Sync Starting: Platform [" + platformSyncInfo.getPlatform().getId() + "]"); log.info("Sync Starting: Platform Top level services [" + platformSyncInfo.getPlatform().getId() + "]"); boolean hadSyncedResources = syncResources(platformResourceSyncInfo.getId(), syncInfos); log.info("Sync Complete: Platform Top level services [" + platformSyncInfo.getPlatform().getId() + "] Local inventory changed: [" + hadSyncedResources + "]"); syncInfos = null; // release to GC // then sync the top level servers by calling back to the server for the sync info for each. We // do this one at a time to avoid forcing the whole inventory into active memory at one time during the sync. Collection<Integer> topLevelServerIds = platformSyncInfo.getTopLevelServerIds(); if (null != topLevelServerIds) { DiscoveryServerService service = configuration.getServerServices().getDiscoveryServerService(); for (Integer topLevelServerId : topLevelServerIds) { syncInfos = service.getResourceSyncInfo(topLevelServerId); if (null != syncInfos) { addAllUuids(syncInfos, allServerSideUuids); log.info("Sync Starting: Top Level Server [" + topLevelServerId + "]"); boolean serverHadSyncedResources = syncResources(topLevelServerId, syncInfos); log.info("Sync Complete: Top Level Server [" + topLevelServerId + "] Local inventory changed: [" + serverHadSyncedResources + "]"); hadSyncedResources = serverHadSyncedResources || hadSyncedResources; syncInfos = null; // release to GC // (jshaughn) Is this really necessary? try { // Wait a little to get other tasks room for breathing Thread.sleep(800L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } purgeObsoleteResources(allServerSideUuids); log.info("Sync Complete: Platform [" + platformSyncInfo.getPlatform().getId() + "]."); // kick off a service scan to scan synced Resources for new child Resources. // // Do this only if we are finished with resource upgrade because no availability checks // or discoveries can happen during upgrade. This is to ensure maximum consistency of the // inventory with the server side as well as to disallow any other server-agent traffic during // the upgrade phase. Not to mention the fact that no thread pools are initialized yet by the // time the upgrade kicks in. if (hadSyncedResources && !isResourceUpgradeActive()) { log.info("Sync changes detected, requesting full availability report and service discovery: Platform [" + platformSyncInfo.getPlatform().getId() + "]"); // Additionally, request a full avail report. This ensures each newly synced resource has its // availability determined on the next avail run (typically < 30s away). Note that this is not // particularly inefficient. A full report is different than a full scan. Although each resource's // latest avail will be in the report sent to the server, only new and eligible resources will have their // avail checked. requestFullAvailabilityReport(); executeServiceScanDeferred(); } } private void addAllUuids(Collection<ResourceSyncInfo> syncInfos, Set<String> allServerSideUuids) { for (ResourceSyncInfo syncInfo : syncInfos) { allServerSideUuids.add(syncInfo.getUuid()); } } /** * Performs a synch on only the single resource and its descendants. This is assumed to be a partial * inventory. To synch on the full inventory call {@link #syncPlatform(PlatformSyncInfo)} * * @param syncInfos the resource's sync data * @return true if any resources needed synchronization, false otherwise */ private boolean syncResources(int rootResourceId, Collection<ResourceSyncInfo> syncInfos) { final long startTime = System.currentTimeMillis(); final Set<Resource> syncedResources = new LinkedHashSet<Resource>(); final Set<ResourceSyncInfo> unknownResourceSyncInfos = new LinkedHashSet<ResourceSyncInfo>(); final Set<Integer> modifiedResourceIds = new LinkedHashSet<Integer>(); final Set<Resource> newlyCommittedResources = new LinkedHashSet<Resource>(); final Set<Resource> ignoredResources = new LinkedHashSet<Resource>(); // rhq-980 Adding agent-side logging to report any unexpected synch failure. try { log.debug("Processing Server sync info..."); processSyncInfo(syncInfos, syncedResources, unknownResourceSyncInfos, modifiedResourceIds, newlyCommittedResources, ignoredResources); if (log.isDebugEnabled()) { log.debug(String.format("DONE Processing sync info: [%d] ms: synced [%d] resources: " + "[%d] unknown, [%d] modified, [%d] newly committed", (System.currentTimeMillis() - startTime), syncedResources.size(), unknownResourceSyncInfos.size(), modifiedResourceIds.size(), newlyCommittedResources.size())); } Set<Resource> unknownResources = mergeUnknownResources(unknownResourceSyncInfos); Set<Resource> modifiedResources = mergeModifiedResources(modifiedResourceIds); purgeIgnoredResources(ignoredResources); postProcessNewlyCommittedResources(newlyCommittedResources); if (log.isDebugEnabled()) { log.debug(String.format("DONE syncing local inventory [%d] ms.", (System.currentTimeMillis() - startTime))); } return !syncedResources.isEmpty() || !unknownResources.isEmpty() || !modifiedResources.isEmpty(); } catch (Throwable t) { log.warn("Failed to synchronize local inventory with Server inventory for Resource [" + rootResourceId + "] and its descendants: " + t.getMessage()); // convert to runtime exception so as not to change the api throw new RuntimeException(t); } } @Override public void synchronizePlatform(PlatformSyncInfo syncInfo) { syncPlatform(syncInfo); performServiceScan(syncInfo.getPlatform().getId()); // NOTE: This will block (the initial scan blocks). // TODO: (jshaughn) should we also request a full avail scan? } @Override public void synchronizeServer(int resourceId, Collection<ResourceSyncInfo> topLevelServerSyncInfo) { log.info("Synchronizing local inventory with Server inventory for Resource [" + resourceId + "] and its descendants..."); syncResources(resourceId, topLevelServerSyncInfo); performServiceScan(resourceId); // NOTE: This will block (the initial scan blocks). // TODO: (jshaughn) should we also request a full avail scan? } /** * Registers the plugin container with a remote server, if there is one. A no-op if we are not talking to a remote * server in which we need to be registered. */ private void registerWithServer() { AgentRegistrar registrar = PluginContainer.getInstance().getAgentRegistrar(); if (registrar != null) { try { registrar.register(10000L); // now that we are registered, let's kick off an inventory report // just to make sure the server has our initial inventory inventoryThreadPoolExecutor.submit((Runnable) this.serverScanExecutor); } catch (Exception e) { log.error("Cannot re-register with the agent, something bad is happening", e); } } } /** * Performs a service scan on the specified Resource, waiting until the scan completes to return. * * @param resourceId the id of the Resource for which to discover child services */ public InventoryReport performServiceScan(int resourceId) { ResourceContainer resourceContainer = getResourceContainer(resourceId); if (resourceContainer == null) { // TODO (ips, 05/16/12): Shouldn't we throw an exception here?? if (log.isDebugEnabled()) { log.debug("No resource container for Resource with id [" + resourceId + "] found - not performing a child service scan."); } return new InventoryReport(agent); } Resource resource = resourceContainer.getResource(); RuntimeDiscoveryExecutor oneTimeExecutor = new RuntimeDiscoveryExecutor(this, configuration, resource); if (log.isDebugEnabled()) { log.debug("Scheduling child service scan for " + resource + " and waiting for it to complete..."); } try { Future<InventoryReport> future = inventoryThreadPoolExecutor .submit((Callable<InventoryReport>) oneTimeExecutor); return future.get(); } catch (Exception e) { throw new RuntimeException("Error submitting child service scan for " + resource + ".", e); } } @Nullable public ResourceComponent<?> getResourceComponent(Resource resource) { ResourceContainer resourceContainer = this.resourceContainersByUUID.get(resource.getUuid()); if (resourceContainer == null) { return null; } return resourceContainer.getResourceComponent(); } @Override public void uninventoryResource(int resourceId) { ResourceContainer resourceContainer = getResourceContainer(resourceId); if (resourceContainer == null) { if (log.isDebugEnabled()) { log.debug("Could not remove Resource [" + resourceId + "] because its container was null."); } return; } boolean scan = removeResourceAndIndicateIfScanIsNeeded(resourceContainer.getResource(), true); //only actually schedule the scanning when we are finished with resource upgrade. The resource upgrade //happens before any scanning infrastructure is established. if (!isResourceUpgradeActive() && scan) { log.info("Deleted resource #[" + resourceId + "] - this will trigger a server scan now"); inventoryThreadPoolExecutor.submit((Runnable) this.serverScanExecutor); } } /** * Removes the resource and its children and returns true if a scan is needed. * * @param resource the Resource to be removed * @return true if this method deleted things that requires a scan. */ boolean removeResourceAndIndicateIfScanIsNeeded(Resource resource, boolean isRoot) { boolean scanIsNeeded = false; this.inventoryLock.writeLock().lock(); try { if (log.isDebugEnabled()) { log.debug("Removing [" + resource + "] from local inventory..."); } // deactivateResource recursively deactivates the resource and its children. No need to do this // if we are recursing as well. if (isRoot) { deactivateResource(resource); } Set<Resource> children = getContainerChildren(resource); // see BZ 801432 if (log.isDebugEnabled()) { if ((children.size() > 0) && (!(children instanceof CopyOnWriteArraySet))) { Exception e = new Exception( "Unexpected child set - if you see this, please notify support or log it in bugzilla. " + children.getClass().getName() + ":" + resource.getId() + ":" + resource.getName() + ":numChildResources=" + children.size()); log.debug("[BZ 801432]", e); } } Set<Resource> tmp = new HashSet<Resource>(children); for (Resource child : tmp) { scanIsNeeded |= removeResourceAndIndicateIfScanIsNeeded(child, false); } Resource parent = resource.getParentResource(); if (parent != null) { parent.removeChildResource(resource); } measurementManager.unscheduleCollection(Collections.singleton(resource.getId())); if (this.resourceContainersByUUID.remove(resource.getUuid()) == null) { if (log.isDebugEnabled()) { log.debug("Asked to remove an unknown Resource [" + resource + "] with UUID [" + resource.getUuid() + "]"); } } else { this.resourceContainerByResourceId.remove(resource.getId()); } // Notify InventoryEventListeners a Resource has been removed. fireResourcesRemoved(Collections.singleton(resource)); // if we just so happened to have removed our top level platform, we need to re-discover it, can't go living without it // once we discover the platform, let's schedule an immediate server scan if ((this.platform == null) || (this.platform.getId() == resource.getId())) { if (log.isDebugEnabled()) { log.debug("Platform [" + resource.getId() + "] was deleted - running platform scan now..."); } this.platform = null; executePlatformScan(); newPlatformWasDeletedRecently = true; scanIsNeeded = true; } else { boolean isTopLevelServer = (this.platform.equals(resource.getParentResource())) && (resource.getResourceType().getCategory() != ResourceCategory.SERVICE); if (isTopLevelServer) { if (log.isDebugEnabled()) { log.debug("Top-level server [" + resource.getId() + "] was deleted - server discovery is needed."); } // if we got here, we just deleted a top level server (whose parent is the platform), let's request a scan scanIsNeeded = true; } } } finally { this.inventoryLock.writeLock().unlock(); } return scanIsNeeded; } /** * Get the parent resource's children, ensuring we use the resource container version of the resource, because * the container's resource is guaranteed to be up to date. * * @param parentResource * @return the children. parentResouce children if there is no container. May be empty. Not null. */ public Set<Resource> getContainerChildren(Resource parentResource) { return getContainerChildren(parentResource, getResourceContainer(parentResource)); } /** * Get the parent resource's children, ensuring we use the resource container version of the resource, because * the container's resource is guaranteed to be up to date. * * @param parentResource * @param container * @return the children, empty if parentContainer is null or there are no children. not null. * @return the children, may be empty, not null. */ public Set<Resource> getContainerChildren(Resource parentResource, ResourceContainer container) { if (null == container) { return parentResource.getChildResources(); } Resource parentContainerResource = container.getResource(); // this is just to log whether there was an reason to actually call this method if (parentResource != parentContainerResource && log.isDebugEnabled()) { Set<Resource> containerChildren = parentContainerResource.getChildResources(); Set<Resource> localChildren = parentResource.getChildResources(); if (containerChildren.equals(localChildren)) { log.debug("Container resource different from local resource.\n Container: " + parentContainerResource + "\n Local: " + parentResource); } else { log.debug("Container resource different from local resource and children differ!\n Container:" + container + containerChildren + "\n Local:" + parentResource + localChildren); } } return parentContainerResource.getChildResources(); } @Override public Resource getPlatform() { return platform; } public Agent getAgent() { return this.agent; } /** * Inject a new availability * * @param resource * @param availabilityType * * @return */ public Availability updateAvailability(Resource resource, AvailabilityType availabilityType) { ResourceContainer resourceContainer = resourceContainersByUUID.get(resource.getUuid()); return resourceContainer.updateAvailability(availabilityType); } public void mergeResourcesFromUpgrade(Set<ResourceUpgradeRequest> upgradeRequests) throws Exception { Set<ResourceUpgradeResponse> serverUpdates = null; try { ServerServices serverServices = this.configuration.getServerServices(); if (serverServices != null) { DiscoveryServerService discoveryServerService = serverServices.getDiscoveryServerService(); serverUpdates = discoveryServerService.upgradeResources(upgradeRequests); } } catch (Exception e) { log.error("Failed to process resource upgrades on the server.", e); throw e; } if (serverUpdates != null) { for (ResourceUpgradeResponse upgradeResponse : serverUpdates) { String resourceKey = upgradeResponse.getUpgradedResourceKey(); String name = upgradeResponse.getUpgradedResourceName(); String description = upgradeResponse.getUpgradedResourceDescription(); String version = upgradeResponse.getUpgradedResourceVersion(); Configuration pluginConfig = upgradeResponse.getUpgradedResourcePluginConfiguration(); //only bother if there's something to upgrade at all on this resource. if (resourceKey != null || name != null || version != null || description != null || pluginConfig != null) { ResourceContainer existingResourceContainer = getResourceContainer(upgradeResponse.getResourceId()); if (existingResourceContainer != null) { Resource existingResource = existingResourceContainer.getResource(); StringBuilder logMessage = new StringBuilder("Resource [").append(existingResource.toString()) .append("] upgraded ["); if (resourceKey != null) { existingResource.setResourceKey(resourceKey); logMessage.append("resourceKey, "); } if (name != null) { existingResource.setName(name); logMessage.append("name, "); } if (version != null) { existingResource.setVersion(version); logMessage.append("version, "); } if (description != null) { existingResource.setDescription(description); logMessage.append("description, "); } if (pluginConfig != null) { existingResource.setPluginConfiguration(pluginConfig); logMessage.append("pluginConfiguration, "); } logMessage.replace(logMessage.length() - 1, logMessage.length(), "] to become [") .append(existingResource.toString()).append("]"); log.info(logMessage.toString()); } else { log.error("Upgraded a resource that is not present on the agent. This should not happen. " + "The id of the missing resource is: " + upgradeResponse.getResourceId()); } } } } } public Resource mergeResourceFromDiscovery(Resource resource, Resource parent) throws PluginContainerException { // If the Resource is already in inventory, make sure its version is up to date. If the version // has been updated make sure the plugin config is up-to-date. Then simply return the existing Resource. Resource existingResource = findMatchingChildResource(resource, parent); if (existingResource != null) { if (mergeExistingResource(existingResource, resource)) { try { refreshResourceComponentState(getResourceContainer(existingResource), true); } catch (Exception e) { log.warn( "Failed to refresh resource component after version change. Resource=" + existingResource + ". Will continue with old container. The resource may not perform as expected until after a plugin container restart.", e); } } return existingResource; } // Auto-generate id and auto-commit if embedded within JBossAS. if (!this.configuration.isInsideAgent()) { resource.setId(this.temporaryKeyIndex.decrementAndGet()); resource.setInventoryStatus(InventoryStatus.COMMITTED); } // Add the Resource to the Resource hierarchy. // (log services at DEBUG, servers and platforms at INFO) String logMessage = String.format("Detected new %s [%s] - adding to local inventory...", resource .getResourceType().getCategory(), resource); if (parent != null) { switch (resource.getResourceType().getCategory()) { case SERVICE: log.debug(logMessage); break; case SERVER: log.info(logMessage); break; case PLATFORM: throw new IllegalStateException( "An attempt was made to add a platform Resource as a child of another Resource."); } parent.addChildResourceWithoutAncestry(resource); } else { if (resource.getResourceType().getCategory() != ResourceCategory.PLATFORM) throw new IllegalStateException( "An attempt was made to add a non-platform Resource as the root Resource."); log.info(logMessage); this.platform = resource; } // Initialize a container for the Resource. ResourceContainer resourceContainer = getResourceContainer(resource); if (resourceContainer != null) { // This should never happen... log.warn("Resource container already existed for Resource that was supposed to be NEW: " + resource); } else { resourceContainer = initResourceContainer(resource); } // Auto-activate if embedded within JBossAS (if within Agent, we need to wait until the Resource has been // imported into the Server's inventory before activating it). if (!this.configuration.isInsideAgent()) { try { activateResource(resource, resourceContainer, true); // just start 'em up as we find 'em for the embedded side } catch (InvalidPluginConfigurationException e) { log.error("Failed to activate " + resource + ": " + e.getLocalizedMessage()); // TODO: I don't think it makes any sense to call the below method w/in the embedded console. // (ips, 07/16/08) handleInvalidPluginConfigurationResourceError(resource, e); } } // Notify InventoryEventListeners a Resource has been added. fireResourcesAdded(Collections.singleton(resource)); return resource; } /** * @param existingResource Current resource for reskey X * @param discoveredResource Discovered resource for reskey X * @return true if the version (and possibly pluginConfig) have been updated in the existing resource. Otherwise false. */ private boolean mergeExistingResource(Resource existingResource, Resource discoveredResource) { String existingVersion = existingResource.getVersion(); String discoveredVersion = discoveredResource.getVersion(); boolean versionChanged = (existingVersion != null) ? !existingVersion.equals(discoveredVersion) : discoveredVersion != null && !discoveredVersion.isEmpty(); if (versionChanged) { if (log.isDebugEnabled()) { log.debug("Discovery reported that version of [" + existingResource + "] changed from [" + existingVersion + "] to [" + discoveredVersion + "]"); } boolean versionUpdated = existingResource.getInventoryStatus() != InventoryStatus.COMMITTED || mergeExistingResourceVersionOnServer(existingResource, discoveredVersion); // If the version has been updated make sure we also update the plugin config if it has been updated. if (versionUpdated) { Configuration mergedPluginConfiguration = mergeExistingResourcePluginConfiguration(existingResource, discoveredResource.getPluginConfiguration()); // Only update the version in local inventory if the server syncs succeeded, otherwise we won't know // to try again the next time this method is called. // TODO: It would be safer to combine the two possible server syncs into one server/transaction if (null != mergedPluginConfiguration) { existingResource.setVersion(discoveredVersion); existingResource.setPluginConfiguration(mergedPluginConfiguration); log.info("Version of [" + existingResource + "] changed from [" + existingVersion + "] to [" + discoveredVersion + "]"); return true; } } } return false; } private boolean mergeExistingResourceVersionOnServer(Resource resource, String newVersion) { boolean versionUpdated = false; ServerServices serverServices = this.configuration.getServerServices(); if (serverServices != null) { try { DiscoveryServerService discoveryServerService = serverServices.getDiscoveryServerService(); discoveryServerService.updateResourceVersion(resource.getId(), newVersion); // Only update the version in local inventory if the server sync succeeded, otherwise we won't know // to try again the next time this method is called. versionUpdated = true; if (log.isDebugEnabled()) { log.debug("New version for [" + resource + "] (" + newVersion + ") was successfully synced to the Server."); } } catch (Exception e) { log.error("Failed to sync-to-Server new version for [" + resource + "]"); } } else { if (log.isDebugEnabled()) { log.debug("Sync-to-Server of new version for [" + resource + "] cannot be done, because Plugin Container is not connected to Server."); } } return versionUpdated; } private Configuration mergeExistingResourcePluginConfiguration(Resource resource, Configuration pluginConfig) { // If there is no update necessary just return the current plugin config of the existing resource Configuration result = resource.getPluginConfiguration(); ConfigurationDefinition configDef = resource.getResourceType().getPluginConfigurationDefinition(); if (null == configDef) { return result; } Map<String, PropertyDefinition> propDefs = configDef.getPropertyDefinitions(); Configuration existingPluginConfig = resource.getPluginConfiguration().deepCopy(false); boolean configChanged = false; // for each property, update the existing plugin config if discovery has set a non-default value for (String propertyName : pluginConfig.getAllProperties().keySet()) { Property discoveredProp = pluginConfig.get(propertyName); // Only overrides the property if it is a read-only property // See BZ: 1379834 PropertyDefinition propDefinition = propDefs.get(propertyName); if (propDefinition != null && propDefinition.isReadOnly()) { if (log.isDebugEnabled()) { log.debug("Discovery reported a new version of " + resource + ". Updating value of config property" + " from [" + existingPluginConfig.get(propertyName) + "] to [" + discoveredProp + "]."); } existingPluginConfig.put(discoveredProp); configChanged = true; } } if (configChanged) { result = mergeExistingResourcePluginConfigurationOnServer(resource, existingPluginConfig); } return result; } private Configuration mergeExistingResourcePluginConfigurationOnServer(Resource resource, Configuration updatedPluginConfig) { Configuration result = null; ServerServices serverServices = this.configuration.getServerServices(); if (serverServices != null) { try { ConfigurationServerService configServerService = serverServices.getConfigurationServerService(); result = configServerService.persistUpdatedPluginConfiguration(resource.getId(), updatedPluginConfig); } catch (Exception e) { log.error("Failed to sync-to-Server new plugin configuration for [" + resource + "]"); } } else { if (log.isDebugEnabled()) { log.debug("Sync-to-Server of new plugin configuration for [" + resource + "] cannot be done, because Plugin Container is not connected to Server."); } } return result; } /** * During initialization time, the inventory manager will active resources after loading them * from disk. Any other manager that starts up and is initialized after the Inventory Manager * is initialized will miss the activation notifications. This method is here so those managers * can be notified during their initialization phase by simply passing in their listener * which will be called for every resource currently activated in inventory. * * @param listener the listener that will be notified for every resource currently active */ public void notifyForAllActivatedResources(InventoryEventListener listener) { List<Resource> activatedResources = new ArrayList<Resource>(); this.inventoryLock.readLock().lock(); try { for (ResourceContainer container : this.resourceContainersByUUID.values()) { if ((container != null) && (container.getResourceComponentState() == ResourceComponentState.STARTED)) { activatedResources.add(container.getResource()); } } for (Resource resource : activatedResources) { try { listener.resourceActivated(resource); } catch (Throwable t) { log.warn("Listener [" + listener + "] of activated resource [" + resource + "] failed", t); } } } finally { this.inventoryLock.readLock().unlock(); activatedResources.clear(); } } private ResourceContainer initResourceContainer(Resource resource) { ResourceContainer resourceContainer = getResourceContainer(resource); if (resourceContainer == null) { ClassLoader classLoader = getResourceClassLoader(resource); resourceContainer = new ResourceContainer(resource, classLoader); if (!this.configuration.isInsideAgent()) { // Auto-sync if the PC is running within the embedded JBossAS console. resourceContainer.setSynchronizationState(ResourceContainer.SynchronizationState.SYNCHRONIZED); } this.resourceContainersByUUID.put(resource.getUuid().intern(), resourceContainer); this.resourceContainerByResourceId.put(resource.getId(), resourceContainer); } else { // container already exists, but make sure the classloader exists too if (resourceContainer.getResourceClassLoader() == null) { ClassLoader classLoader = getResourceClassLoader(resource); resourceContainer.setResourceClassLoader(classLoader); } } return resourceContainer; } private ClassLoader getResourceClassLoader(Resource resource) { ClassLoader classLoader; try { classLoader = pluginFactory.getResourceClassloader(resource); } catch (PluginContainerException e) { if (log.isTraceEnabled()) { log.trace("Access to resource [" + resource + "] will fail due to missing classloader.", e); } else if (log.isDebugEnabled()) { log.debug("Access to resource [" + resource + "] will fail due to missing classloader - cause: " + e); } classLoader = null; } return classLoader; } /** * This method prepares the resource and container for activation. * <p> * After this method has processed the resource and container, it is enough * to call ResourceComponent.start(). All the datastructures needed for that * call are prepared in the container by this method. * * @param resource the resource that we are activating * @param container the container to hold the datastructures * @throws InvalidPluginConfigurationException * @throws PluginContainerException * @return true the resource has been successfully prepared and can be started. False if the resource should not be started. */ private boolean prepareResourceForActivation(Resource resource, @NotNull ResourceContainer container, boolean forceReinitialization) throws InvalidPluginConfigurationException, PluginContainerException { // don't bother doing anything if this resource is of a disabled/ignored type if (this.pluginManager.getMetadataManager().isDisabledOrIgnoredResourceType(resource.getResourceType())) { return false; } if (resourceUpgradeDelegate.hasUpgradeFailed(resource)) { if (log.isTraceEnabled()) { log.trace("Skipping activation of " + resource + " - it has failed to upgrade."); } return false; } ResourceComponent component = container.getResourceComponent(); ResourceComponentState state = container.getResourceComponentState(); // state is a transient field, so reinitialize it just in case this is invoked just after loadFromDisk() if (state == null) { container.setResourceComponentState(ResourceComponentState.STOPPED); state = ResourceComponentState.STOPPED; } // if the component exists and is not stopped then we may not have to do anything if ((component != null) && (state != ResourceComponentState.STOPPED)) { // if STARTED and we are forced to restart (e.g. plugin config change), then stop the component // and continue. If STARTING just let it continue to start as interruption could put us in a bad state. if (forceReinitialization) { switch (state) { case STARTED: if (log.isDebugEnabled()) { log.debug("Forcing re-initialization of an already started resource: " + resource); } deactivateResource(resource); break; case STARTING: log.warn("Could not force initialization of component for resource [" + resource.getId() + "] as it is already in the process of starting."); return false; default: log.error("Unexpected state [" + state.name() + "], returning false..."); return false; } } else { if (log.isTraceEnabled()) { log.trace("No need to prepare the activation of resource " + resource + " - its component is already started and its plugin " + "config has not been updated since it was last started."); } return false; } } if (log.isDebugEnabled()) { log.debug("Preparing component for [" + resource + "] for activation, current state=[" + container.getResourceComponentState() + "], forcing reinitialization=[" + forceReinitialization + "]..."); } ResourceContainer parentResourceContainer; Resource parentResource = resource.getParentResource(); if (parentResource == null) { parentResourceContainer = null; } else { parentResourceContainer = getResourceContainer(parentResource); if (parentResourceContainer == null) { // The parent probably just got uninventoried - log a DEBUG message and abort. if (log.isDebugEnabled()) { log.debug(resource + " not being prepared for activation - container not found for parent " + parentResource + "."); } return false; } } // If the component does not even exist yet, we need to instantiate it and set it on the container. if (component == null) { if (log.isDebugEnabled()) { log.debug("Creating component for [" + resource + "]..."); } try { // should not throw ResourceTypeNotEnabledException because we checked for that above - if it does, just handle it as an error component = pluginFactory.buildResourceComponent(resource); } catch (Throwable e) { throw new PluginContainerException("Could not build component for Resource [" + resource + "]", e); } container.setResourceComponent(component); } // Start the resource, but only if its parent component is running. If the parent is null, that means // the resource is, itself, the root platform, which we always activate. boolean isParentStarted = (parentResourceContainer == null) || (parentResourceContainer.getResourceComponentState() == ResourceComponentState.STARTED); if (isParentStarted) { ResourceType type = resource.getResourceType(); // wrap the discovery component in a proxy to allow us to timeout discovery invocations ResourceDiscoveryComponent discoveryComponent; try { // should not throw ResourceTypeNotEnabledException because we checked for that above - if it does, just handle it as an error discoveryComponent = pluginFactory.getDiscoveryComponent(type, parentResourceContainer); discoveryComponent = this.discoveryComponentProxyFactory.getDiscoveryComponentProxy(type, discoveryComponent, getDiscoveryComponentTimeout(), parentResourceContainer); } catch (Exception e) { discoveryComponent = null; log.warn("Cannot give activated resource its discovery component. Cause: " + e); } ConfigurationUtility.normalizeConfiguration(resource.getPluginConfiguration(), type.getPluginConfigurationDefinition(), false, false); ResourceComponent<?> parentComponent = null; ResourceContext<?> parentResourceContext = null; if (parentResource != null) { ResourceContainer rc = getResourceContainer(parentResource); parentComponent = rc.getResourceComponent(); parentResourceContext = rc.getResourceContext(); } ResourceContext context = createResourceContext(resource, parentComponent, parentResourceContext, discoveryComponent); container.setResourceContext(context); return true; } else { if (log.isDebugEnabled()) { log.debug("Resource [" + resource + "] not being prepared for activation; parent isn't started: " + parentResourceContainer); } return false; } } /** * This will start the resource's plugin component, creating it first if it has not yet been created. If the * component is already created and started, this method is a no-op. * * @param resource the resource that the component will manage * @param container the wrapper around the resource and its component * @param updatedPluginConfig if <code>true</code>, this will indicate that the resource's plugin configuration is * known to have changed since the last time the resource component was started * * @throws InvalidPluginConfigurationException when connecting to the managed resource fails due to an invalid * plugin configuration * @throws PluginContainerException for all other errors */ public void activateResource(Resource resource, @NotNull ResourceContainer container, boolean updatedPluginConfig) throws InvalidPluginConfigurationException, PluginContainerException { if (resourceUpgradeDelegate.hasUpgradeFailed(resource)) { if (log.isTraceEnabled()) { log.trace("Skipping activation of " + resource + " - it has failed to upgrade."); } return; } if (prepareResourceForActivation(resource, container, updatedPluginConfig)) { container.setResourceComponentState(ResourceComponentState.STARTING); ResourceContext context; ResourceComponent component; try { context = container.getResourceContext(); // Wrap the component in a proxy that will provide locking and a timeout for the call to start(). component = container.createResourceComponentProxy(ResourceComponent.class, FacetLockType.READ, COMPONENT_START_TIMEOUT, true, false, true); } catch (Throwable t) { container.setResourceComponentState(ResourceComponentState.STOPPED); throw new PluginContainerException("Failed getting proxy for resource " + resource + ".", t); } try { component.start(context); container.setResourceComponentState(ResourceComponentState.STARTED); resource.setConnected(true); // This tells the server-side that the resource has connected successfully. } catch (Throwable t) { // Don't leave in a STARTING state. Don't actually call component.stop(), // because we're not actually STARTED container.setResourceComponentState(ResourceComponentState.STOPPED); StringBuilder messageBuilder = new StringBuilder("Failed to start component for ").append(resource); if (isNotBlank(t.getMessage())) { messageBuilder.append(" - ").append(t.getMessage()); } if (t.getCause() != null) { messageBuilder.append(" - Cause: ").append(t.getClass().getName()); if (isNotBlank(t.getCause().getMessage())) { messageBuilder.append(": ").append(t.getCause().getMessage()); } } String message = messageBuilder.toString(); if (updatedPluginConfig || (t instanceof InvalidPluginConfigurationException)) { if (log.isDebugEnabled()) { log.debug("Resource has a bad config, waiting for this to go away: " + resource); } InventoryEventListener iel = new ResourceGotActivatedListener(); addInventoryEventListener(iel); throw new InvalidPluginConfigurationException(message); } throw new PluginContainerException(message); } // We purposefully do not get availability of this resource yet // We need availability checked during the normal availability executor timeframe. // Otherwise, new resources will not have their availabilities shipped up to the server because // they will look like they haven't changed status since the last avail report - but the new // resources statuses never got sent up in the last avail report because they didn't exist at that time // Finally, inform the rest of the plugin container that this resource has been activated fireResourceActivated(resource); } } /** * @return The location for [plugins] to write data files */ public File getDataDirectory() { return this.configuration.getDataDirectory(); } private <T extends ResourceComponent<?>> ResourceContext<T> createResourceContext(Resource resource, T parentComponent, ResourceContext<?> parentResourceContext, ResourceDiscoveryComponent<T> discoveryComponent) { return new ResourceContext<T>(resource, // the resource itself parentComponent, // its parent component parentResourceContext, //the resource context of the parent discoveryComponent, // the discovery component (this is actually the proxy to it) SystemInfoFactory.createSystemInfo(), // for native access this.configuration.getTemporaryDirectory(), // location for plugin to write temp files this.configuration.getDataDirectory(), // base location for plugin to write data files this.configuration.getContainerName(), // the name of the agent/PC getEventContext(resource), // for event access getOperationContext(resource), // for operation manager access getContentContext(resource), // for content manager access getAvailabilityContext(resource), getInventoryContext(resource), this.configuration.getPluginContainerDeployment(), // helps components make determinations of what to do new ComponentInvocationContextImpl()); } public <T extends ResourceComponent<?>> ResourceUpgradeContext<T> createResourceUpgradeContext(Resource resource, ResourceContext<?> parentResourceContext, T parentComponent, ResourceDiscoveryComponent<T> discoveryComponent) { return new ResourceUpgradeContext<T>(resource, // the resource itself parentResourceContext, //the context of its parent resource parentComponent, // its parent component discoveryComponent, // the discovery component (this is actually the proxy to it) SystemInfoFactory.createSystemInfo(), // for native access this.configuration.getTemporaryDirectory(), // location for plugin to write temp files this.configuration.getDataDirectory(), // base location for plugin to write data files this.configuration.getContainerName(), // the name of the agent/PC getEventContext(resource), // for event access getOperationContext(resource), // for operation manager access getContentContext(resource), // for content manager access getAvailabilityContext(resource), // for components that want avail manager access getInventoryContext(resource), this.configuration.getPluginContainerDeployment()); // helps components make determinations of what to do } /** * This will send a resource error to the server (if applicable) to indicate that the given resource could not be * connected to due to an invalid plugin configuration. * * @param resource the resource that could not be connected to * @param t the exception that indicates the problem with the plugin configuration * @return true if the error was sent successfully, or false otherwise */ public boolean handleInvalidPluginConfigurationResourceError(Resource resource, Throwable t) { resource.setConnected(false); // invalid plugin configuration infers the resource component is disconnected // Give the server-side an error message describing the connection failure that can be // displayed on the resource's Inventory page. ResourceError resourceError = new ResourceError(resource, ResourceErrorType.INVALID_PLUGIN_CONFIGURATION, t.getLocalizedMessage(), ThrowableUtil.getStackAsString(t), System.currentTimeMillis()); return sendResourceErrorToServer(resourceError); } boolean sendResourceErrorToServer(ResourceError resourceError) { Resource resource = resourceError.getResource(); if (resource.getId() == 0) { if (log.isDebugEnabled()) { log.debug("Resource ID is 0!" + " Will not send resource error until the resource is synced with server. " + resource.toString()); } return false; } boolean errorSent = false; DiscoveryServerService serverService = null; ServerServices serverServices = this.configuration.getServerServices(); if (serverServices != null) { serverService = serverServices.getDiscoveryServerService(); } if (serverService != null) { try { // use light-weight proxy to Resource, so that the entire hierarchy doesn't get serialized resourceError.setResource(new Resource(resource.getId())); serverService.setResourceError(resourceError); errorSent = true; } catch (RuntimeException e) { log.warn("Cannot inform the Server about a Resource error [" + resourceError + "]. Cause: " + e); } } return errorSent; } private Resource findMatchingChildResource(Resource resource, Resource parent) { if (resource != null) { if (parent == null) { // Resource must be a platform - see if it matches our local platform if (this.platform != null && matches(resource, this.platform)) { return this.platform; } } else { // don't use container children here, the caller is providing the desired resources for (Resource child : parent.getChildResources()) { if (child != null && matches(resource, child)) { return child; } } } } return null; } private boolean matches(Resource newResource, Resource existingResource) { try { return ((existingResource.getId() != 0) && (existingResource.getId() == newResource.getId())) || (existingResource.getUuid().equals(newResource.getUuid())) || (existingResource.getResourceType().equals(newResource.getResourceType()) && existingResource .getResourceKey().equals(newResource.getResourceKey())); } catch (RuntimeException e) { log.error("Runtime error while attempting to compare existing Resource " + existingResource + " to new Resource " + newResource); throw e; } } /** * Lookup all the resources with a particular type * * @param type the type to match against * * @return the set of resources matching the provided type */ public Set<Resource> getResourcesWithType(ResourceType type) { return getResourcesWithType(type, getContainerChildren(this.platform)); } private Set<Resource> getResourcesWithType(ResourceType type, Set<Resource> resources) { Set<Resource> result = new HashSet<Resource>(); if (resources == null) { return result; } for (Resource resource : resources) { // If we're looking for SERVERs and we've hit a SERVICE, skip it as it doesn't match and // we won't find SERVERs below a SERVICE if (ResourceCategory.SERVER == type.getCategory() && ResourceCategory.SERVICE == resource.getResourceType().getCategory()) { continue; } Set<Resource> children = getContainerChildren(resource); result.addAll(getResourcesWithType(type, children)); if (type.equals(resource.getResourceType())) { ResourceContainer container = getResourceContainer(resource); result.add((container == null) ? resource : container.getResource()); } } return result; } public boolean isDiscoveryScanInProgress() { return (this.inventoryThreadPoolExecutor.getActiveCount() >= 1); } /** * Tries to load an existing inventory from the file data/inventory.dat */ private void loadFromDisk() { this.inventoryLock.writeLock().lock(); File file = null; try { file = new File(this.configuration.getDataDirectory(), "inventory.dat"); if (file.exists()) { long start = System.currentTimeMillis(); log.info("Loading inventory from data file [" + file + "]..."); InventoryFile inventoryFile = new InventoryFile(file, this); inventoryFile.loadInventory(); this.platform = inventoryFile.getPlatform(); practiceSafeSets(this.platform); this.resourceContainersByUUID.clear(); this.resourceContainerByResourceId.clear(); for (String uuid : inventoryFile.getResourceContainers().keySet()) { ResourceContainer resourceContainer = inventoryFile.getResourceContainers().get(uuid); this.resourceContainersByUUID.put(uuid, resourceContainer); Resource resource = resourceContainer.getResource(); practiceSafeSets(resource); this.resourceContainerByResourceId.put(resource.getId(), resourceContainer); compactResource(resource); } log.info("Inventory with size [" + this.resourceContainersByUUID.size() + "] loaded from data file in [" + (System.currentTimeMillis() - start) + "ms]"); } } catch (Exception e) { this.platform = null; this.resourceContainersByUUID.clear(); this.resourceContainerByResourceId.clear(); if (file != null) { file.renameTo(new File(file.getAbsolutePath() + ".invalid")); // move it out of the way if we can, retain it for later analysis } log.error( "Could not load inventory from data file. The agent has lost knowledge of its previous inventory - " + "it will resync its inventory once it can reconnect with a server.", e); } finally { this.inventoryLock.writeLock().unlock(); } } // Make sure the child resources are in our desired Set impl private void practiceSafeSets(final Resource resource) { Set<Resource> children = resource.getChildResources(); if (null == children) { resource.setChildResources(new CopyOnWriteArraySet<Resource>()); } else if (!(children instanceof CopyOnWriteArraySet)) { if (log.isDebugEnabled()) { log.debug("Converting persisted childResources to CopyOnWriteArraySet from [" + children.getClass().getSimpleName() + "] for [" + resource.getName() + "]"); } resource.setChildResources(new CopyOnWriteArraySet<Resource>(children)); } } /** * Shutdown the ResourceComponents from the bottom up. * @param resource The resource to deactivate */ public void deactivateResource(Resource resource) { this.inventoryLock.writeLock().lock(); try { ResourceContainer container = getResourceContainer(resource); if ((container != null) && (container.getResourceComponentState() == ResourceComponentState.STARTED)) { // traverse the hierarchy using the container's resource, which should be up to date for (Resource child : getContainerChildren(resource, container)) { deactivateResource(child); } try { ResourceComponent<?> component = container.createResourceComponentProxy(ResourceComponent.class, FacetLockType.WRITE, COMPONENT_STOP_TIMEOUT, true, true, true); component.stop(); if (log.isDebugEnabled()) { log.debug("Successfully deactivated resource with id [" + resource.getId() + "]."); } } catch (Throwable t) { log.warn("Plugin Error: Failed to stop component for [" + resource + "].", t); } container.setResourceComponentState(ResourceComponentState.STOPPED); if (log.isDebugEnabled()) { log.debug("Set component state to STOPPED for resource with id [" + resource.getId() + "]."); } fireResourceDeactivated(resource); } } finally { this.inventoryLock.writeLock().unlock(); } } private void persistToDisk() { try { deactivateResource(this.platform); File dataDir = this.configuration.getDataDirectory(); if (!dataDir.exists()) { if (!dataDir.mkdirs()) { throw new RuntimeException("Failed to create data directory [" + dataDir + "]."); } } File file = new File(dataDir, "inventory.dat"); InventoryFile inventoryFile = new InventoryFile(file, this); inventoryFile.storeInventory(this.platform, this.resourceContainersByUUID); } catch (Exception e) { log.error("Could not persist inventory data to disk", e); } } /** * Detects the top platform resource and starts its ResourceComponent. * * TODO GH: Move this to another class (this one is getting too big) * @return The discovered platform (which might be a dummy in case of testing) */ private Resource discoverPlatform() { SystemInfo systemInfo = SystemInfoFactory.createSystemInfo(); PluginMetadataManager metadataManager = pluginManager.getMetadataManager(); Set<ResourceType> platformTypes = metadataManager.getTypesForCategory(ResourceCategory.PLATFORM); // This should only ever have 1 or, at most, 2 Resources // (always the Java fallback platform, and the native platform if supported). Set<DiscoveredResourceDetails> allDiscoveredPlatforms = new HashSet<DiscoveredResourceDetails>(2); if ((platformTypes != null) && (platformTypes.size() > 0)) { //check for fake testing type. If the test platform type is being used, it is always going to be //the sole platform type available. if (platformTypes.size() == 1 && platformTypes.contains(PluginMetadataManager.TEST_PLATFORM_TYPE)) { return getTestPlatform(); } // Go through all the platform types that are supported and see if they can detect our platform. for (ResourceType platformType : platformTypes) { try { ResourceDiscoveryComponent component = pluginFactory.getDiscoveryComponent(platformType, null); ResourceDiscoveryContext context = new ResourceDiscoveryContext(platformType, null, null, systemInfo, Collections.EMPTY_LIST, Collections.EMPTY_LIST, configuration.getContainerName(), this.configuration.getPluginContainerDeployment()); Set<DiscoveredResourceDetails> discoveredResources = null; try { discoveredResources = invokeDiscoveryComponent(null, component, context); } catch (DiscoverySuspendedException e) { log.error("Discovery seems to be suspended for platforms due to upgrade error.", e); } catch (Throwable e) { log.warn("Platform plugin discovery failed - skipping", e); } if (discoveredResources != null) { allDiscoveredPlatforms.addAll(discoveredResources); } } catch (ResourceTypeNotEnabledException rtne) { if (log.isDebugEnabled()) { log.debug("Skipping platform discovery - its type is disabled: " + platformType); } } catch (Throwable e) { log.error("Error in platform discovery", e); } } } else { // This is very strange - there are no platform types - we should never be missing the built-in platform plugin. log.error("Missing platform plugin(s) - falling back to dummy platform impl; this should only occur in tests!"); // TODO: Set sysprop (e.g. rhq.test.mode=true) in integration tests, // and throw a runtime exception here if that sysprop is not set. return getTestPlatform(); } if (allDiscoveredPlatforms.isEmpty()) { throw new IllegalStateException("Neither a native nor a Java platform was discovered - " + "this should never happen. Known platform types are " + platformTypes + "."); } if (allDiscoveredPlatforms.size() > 2) { log.warn("Platform discovery reported too many platforms - " + "the platform discovery components for platform types " + platformTypes + " " + "should be fixed so together they report no more than 2 platforms total. " + "Reported platforms: " + allDiscoveredPlatforms + "."); } DiscoveredResourceDetails javaPlatform = null; DiscoveredResourceDetails nativePlatform = null; for (DiscoveredResourceDetails discoveredPlatform : allDiscoveredPlatforms) { // We know the Java resource type in the descriptor is named "Java". if (discoveredPlatform.getResourceType().getName().equalsIgnoreCase("Java")) { javaPlatform = discoveredPlatform; } else { nativePlatform = discoveredPlatform; } } // In most cases, we will have both (since we support most platforms natively), // so use the native platform if we have it; if not, fall back to the Java platform. DiscoveredResourceDetails platformToUse = (nativePlatform != null) ? nativePlatform : javaPlatform; // Build our actual platform resource now that we've discovered it. Resource platform = createNewResource(platformToUse); platform.setAgent(this.agent); return platform; } /** * If for some reason the platform plugin is not available, this method can be called to add a "dummy" platform * resource. This is normally only used during tests. * @return A dummy platform for testing purposes only. */ private Resource getTestPlatform() { ResourceType type = this.pluginManager.getMetadataManager().addTestPlatformType(); if (this.platform != null && this.platform.getResourceType() == type) { return this.platform; } Set<Resource> childResources = new CopyOnWriteArraySet<Resource>( Collections.newSetFromMap(new ConcurrentHashMap<Resource, Boolean>())); Resource platform = new Resource(childResources); platform.setResourceKey("testkey" + configuration.getContainerName()); platform.setName("testplatform"); platform.setResourceType(type); platform.setUuid(UUID.randomUUID().toString()); platform.setAgent(this.agent); return platform; } /** * This method is called for a resource tree that exists in the server inventory but * not in the agent's inventory. * * @param resource */ private void syncSchedulesRecursively(Resource resource) { if (resource.getInventoryStatus() == InventoryStatus.COMMITTED) { if (resource.getResourceType().getCategory() == ResourceCategory.PLATFORM) { // Get and schedule the latest measurement schedules rooted at the given id. // This should include disabled schedules to make sure that previously enabled schedules are shut off. Set<ResourceMeasurementScheduleRequest> scheduleRequests = configuration.getServerServices() .getMeasurementServerService().getLatestSchedulesForResourceId(resource.getId(), false); installSchedules(scheduleRequests); // performing syncing of the children schedules in one fell swoop Set<Integer> childrenIds = new HashSet<Integer>(); for (Resource child : getContainerChildren(resource)) { childrenIds.add(child.getId()); } scheduleRequests = configuration.getServerServices().getMeasurementServerService() .getLatestSchedulesForResourceIds(childrenIds, true); installSchedules(scheduleRequests); } else { Set<ResourceMeasurementScheduleRequest> scheduleRequests = configuration.getServerServices() .getMeasurementServerService().getLatestSchedulesForResourceId(resource.getId(), true); installSchedules(scheduleRequests); } } } private void syncDriftDefinitionsRecursively(Resource resource) { if (resource.getInventoryStatus() != InventoryStatus.COMMITTED) { return; } Deque<Resource> resources = new LinkedList<Resource>(); resources.push(resource); Set<Integer> resourceIds = new HashSet<Integer>(); while (!resources.isEmpty()) { Resource r = resources.pop(); if (supportsDriftManagement(r)) { resourceIds.add(r.getId()); } Set<Resource> children = getContainerChildren(r); for (Resource child : children) { resources.push(child); } } DriftSyncManager driftSyncMgr = createDriftSyncManager(); driftSyncMgr.syncWithServer(resourceIds); } private boolean supportsDriftManagement(Resource r) { PluginMetadataManager metaDataMgr = this.pluginManager.getMetadataManager(); ResourceType type = metaDataMgr.getType(r.getResourceType()); return type != null && type.getDriftDefinitionTemplates() != null && !type.getDriftDefinitionTemplates().isEmpty(); } private void syncSchedules(Set<Resource> resources) { if (log.isDebugEnabled()) { log.debug("syncSchedules(Set<Resource>) for resources: " + resources); } if (resources.isEmpty()) { return; } Set<Integer> committedResourceIds = new HashSet<Integer>(); for (Resource resource : resources) { if (resource.getInventoryStatus() == InventoryStatus.COMMITTED) { committedResourceIds.add(resource.getId()); } } Set<ResourceMeasurementScheduleRequest> scheduleRequests = configuration.getServerServices() .getMeasurementServerService().getLatestSchedulesForResourceIds(committedResourceIds, false); installSchedules(scheduleRequests); } private void syncDriftDefinitions(Set<Resource> resources) { if (log.isDebugEnabled()) { log.debug("Syncing drift definitions for " + resources); } if (resources.isEmpty()) { return; } Set<Integer> committedResourceIds = new HashSet<Integer>(); for (Resource resource : resources) { if (resource.getInventoryStatus() == InventoryStatus.COMMITTED) { committedResourceIds.add(resource.getId()); } } DriftSyncManager driftSyncMgr = createDriftSyncManager(); driftSyncMgr.syncWithServer(committedResourceIds); } private void postProcessNewlyCommittedResources(Set<Resource> resources) { if (log.isDebugEnabled()) { log.debug("Post-processing newly committed resources: " + resources); } if (resources.isEmpty()) { return; } Set<Integer> newlyCommittedResourceIds = new HashSet<Integer>(); for (Resource resource : resources) { if (resource.getInventoryStatus() == InventoryStatus.COMMITTED) { newlyCommittedResourceIds.add(resource.getId()); } } Set<ResourceMeasurementScheduleRequest> schedules = configuration.getServerServices() .getDiscoveryServerService().postProcessNewlyCommittedResources(newlyCommittedResourceIds); installSchedules(schedules); } private void installSchedules(Set<ResourceMeasurementScheduleRequest> scheduleRequests) { this.measurementManager.scheduleCollection(scheduleRequests); } private DriftSyncManager createDriftSyncManager() { DriftSyncManager mgr = new DriftSyncManager(); mgr.setDriftServer(configuration.getServerServices().getDriftServerService()); mgr.setDataDirectory(configuration.getDataDirectory()); mgr.setDriftManager(PluginContainer.getInstance().getDriftManager()); mgr.setInventoryManager(this); return mgr; } @Override public void requestFullAvailabilityReport() { availabilityExecutor.sendFullReportNextTime(); } /** * Instructs the inventory manager to notify the specified listener of inventory change events. * * @param listener instance to notify of change events */ public void addInventoryEventListener(InventoryEventListener listener) { this.inventoryEventListeners.add(listener); } /** * Removes the specified listener from notification of inventory change events. * * @param listener instance to remove from event notification */ public void removeInventoryEventListener(InventoryEventListener listener) { this.inventoryEventListeners.remove(listener); } /** * @return true if the inventory manager failed to merge the upgrade requests with the server during startup. */ public boolean hasUpgradeMergeFailed() { return resourceUpgradeDelegate.hasUpgradeMergeFailed(); } /** * The resource upgrade should only occur during the {@link #initialize()} method and should be * switched off at all other times. * * @return true if resource upgrade is currently active, false otherwise */ public boolean isResourceUpgradeActive() { return resourceUpgradeDelegate.enabled(); } /** * Always use this before accessing the event listeners because this ensures * thread safety. * * @return all inventory event listeners */ private Set<InventoryEventListener> getInventoryEventListeners() { return this.inventoryEventListeners; } /** * Notifies all inventory listeners that the specified resources have been added to the inventory. * * @param resources resources that were added to trigger this event; will not fire an event if this is <code> * null</code> */ void fireResourcesAdded(Set<Resource> resources) { if (resources == null) { return; } Set<InventoryEventListener> iteratorSafeListeners = getInventoryEventListeners(); for (InventoryEventListener listener : iteratorSafeListeners) { // Catch anything to make sure we don't stop firing to other listeners try { listener.resourcesAdded(resources); } catch (Throwable t) { log.error("Error while invoking resources added event on listener", t); } } } void fireResourceActivated(Resource resource) { if ((resource == null) || (resource.getId() == 0)) { if (log.isDebugEnabled()) { log.debug("Not firing activated event for resource: " + resource); } return; } if (log.isDebugEnabled()) { log.debug("Firing activated for resource: " + resource); } Set<InventoryEventListener> iteratorSafeListeners = getInventoryEventListeners(); for (InventoryEventListener listener : iteratorSafeListeners) { // Catch anything to make sure we don't stop firing to other listeners try { listener.resourceActivated(resource); } catch (Throwable t) { log.error("Error while invoking resource activated event on listener", t); } } } private void fireResourceDeactivated(Resource resource) { if ((resource == null) || (resource.getId() == 0)) { if (log.isDebugEnabled()) { log.debug("Not firing deactivated event for resource: " + resource); } return; } if (log.isDebugEnabled()) { log.debug("Firing deactivated for resource: " + resource); } Set<InventoryEventListener> iteratorSafeListeners = getInventoryEventListeners(); for (InventoryEventListener listener : iteratorSafeListeners) { // Catch anything to make sure we don't stop firing to other listeners try { listener.resourceDeactivated(resource); } catch (Throwable t) { log.error("Error while invoking resource deactivated event on listener", t); } } } /** * Notifies all inventory listeners that the specified resources have been removed from the inventory. * * @param resources resources that were removed to trigger this event; will not fire an event if this is <code> * null</code> */ void fireResourcesRemoved(Set<Resource> resources) { if (resources == null) { return; } Set<InventoryEventListener> iteratorSafeListeners = getInventoryEventListeners(); for (InventoryEventListener listener : iteratorSafeListeners) { // Catch anything to make sure we don't stop firing to other listeners try { listener.resourcesRemoved(resources); } catch (Throwable t) { log.error("Error while invoking resources removed event on listener", t); } } } @Override public void enableServiceScans(int serverResourceId, Configuration config) { throw new UnsupportedOperationException("not implemented yet"); // TODO: Implement this method. } @Override public void disableServiceScans(int serverResourceId) { throw new UnsupportedOperationException("not implemented yet"); // TODO: Implement this method. } @NotNull Set<Resource> executeComponentDiscovery(ResourceType resourceType, ResourceDiscoveryComponent discoveryComponent, ResourceContainer parentContainer, List<ProcessScanResult> processScanResults) { ResourceContext parentResourceContext = parentContainer.getResourceContext(); ResourceComponent parentComponent = parentContainer.getResourceComponent(); Resource parentResource = parentContainer.getResource(); long startTime = System.currentTimeMillis(); if (log.isDebugEnabled()) { log.debug("Executing discovery for [" + resourceType.getName() + "] Resources..."); } Set<Resource> newResources; try { ResourceDiscoveryContext context = new ResourceDiscoveryContext(resourceType, parentComponent, parentResourceContext, SystemInfoFactory.createSystemInfo(), processScanResults, Collections.EMPTY_LIST, this.configuration.getContainerName(), this.configuration.getPluginContainerDeployment()); newResources = new HashSet<Resource>(); try { Set<DiscoveredResourceDetails> discoveredResources = invokeDiscoveryComponent(parentContainer, discoveryComponent, context); if ((discoveredResources != null) && (!discoveredResources.isEmpty())) { IdentityHashMap<Configuration, DiscoveredResourceDetails> pluginConfigObjects = new IdentityHashMap<Configuration, DiscoveredResourceDetails>(); for (DiscoveredResourceDetails discoveredResource : discoveredResources) { if (discoveredResource == null) { throw new IllegalStateException("Plugin error: Discovery class " + discoveryComponent.getClass().getName() + " returned a Set containing one or more null items."); } if (!discoveredResource.getResourceType().equals(resourceType)) { throw new IllegalStateException("Plugin error: Discovery class " + discoveryComponent.getClass().getName() + " returned a DiscoveredResourceDetails with an incorrect ResourceType (was " + discoveredResource.getResourceType().getName() + " but should have been " + resourceType.getName()); } if (null != pluginConfigObjects.put(discoveredResource.getPluginConfiguration(), discoveredResource)) { throw new IllegalStateException( "The plugin component " + discoveryComponent.getClass().getName() + " returned multiple resources that point to the same plugin configuration object on the " + "resource type [" + resourceType + "]. This is not allowed, please use " + "ResourceDiscoveryContext.getDefaultPluginConfiguration() " + "for each discovered resource."); } Resource newResource = InventoryManager.createNewResource(discoveredResource); newResources.add(newResource); } } } catch (DiscoverySuspendedException e) { //ok, the discovery is suspended for this resource type under this parent. //but we can continue the discovery in the child resource types of the existing resources. //we can therefore pretend that the discovery returned the existing resources so that //we can recurse into their children up in the call-chain. for (Resource existingResource : getContainerChildren(parentResource)) { if (resourceType.equals(existingResource.getResourceType())) { newResources.add(existingResource); } } } } catch (Throwable e) { // TODO GH: Add server/parent - up/down semantics so this won't happen just because a server is not up long elapsedTime = System.currentTimeMillis() - startTime; if (!PluginContainer.getInstance().isRunning()) { log.warn("Could not complete discovery, plugin container was shut down."); } else { log.warn("Failure during discovery for [" + resourceType.getName() + "] Resources - failed after " + elapsedTime + " ms.", e); } return Collections.EMPTY_SET; } long elapsedTime = System.currentTimeMillis() - startTime; if (elapsedTime > 20000) log.info("Discovery for [" + resourceType.getName() + "] resources took [" + elapsedTime + "] ms"); else log.debug("Discovery for [" + resourceType.getName() + "] resources completed in [" + elapsedTime + "] ms"); return newResources; } @Nullable private EventContext getEventContext(Resource resource) { EventContext eventContext; if (resource.getResourceType().getEventDefinitions() != null && !resource.getResourceType().getEventDefinitions().isEmpty()) { eventContext = new EventContextImpl(resource, eventManager); } else { eventContext = null; } return eventContext; } private OperationContext getOperationContext(Resource resource) { if (resource.getResourceType().getOperationDefinitions() == null || resource.getResourceType().getOperationDefinitions().isEmpty()) { return null; } if (resource.getId() == 0) { log.info("Resource ID is 0! Operation features will not work until the resource is synced with server"); } return new OperationContextImpl(resource.getId(), operationManager); } private ContentContext getContentContext(Resource resource) { ResourceType resourceType = resource.getResourceType(); boolean hasPackageTypes = (resourceType.getPackageTypes() != null && !resourceType.getPackageTypes().isEmpty()); boolean hasContentBasedCreatableChildren = false; if (resourceType.getChildResourceTypes() != null) { for (ResourceType childResourceType : resourceType.getChildResourceTypes()) { if (childResourceType.isCreatable() && childResourceType.getCreationDataType() == ResourceCreationDataType.CONTENT) { hasContentBasedCreatableChildren = true; break; } } } // Only give the ResourceComponent a ContentContext if its metadata says it actually needs one. if (!hasPackageTypes && !hasContentBasedCreatableChildren) { return null; } if (resource.getId() == 0) { log.info("Resource ID is 0! Content features will not work until the resource is synced with server"); } return new ContentContextImpl(resource.getId(), contentManager); } private AvailabilityContext getAvailabilityContext(Resource resource) { if (null == resource.getUuid() || resource.getUuid().isEmpty()) { log.error("RESOURCE UUID IS NOT SET! Availability features may not work!"); } AvailabilityContext availabilityContext = new AvailabilityContextImpl(resource, this); return availabilityContext; } /** * Create inventory context for a resource. * * @param resource the resource * @return the inventory context */ private InventoryContext getInventoryContext(Resource resource) { if (null == resource.getUuid() || resource.getUuid().isEmpty()) { log.error("RESOURCE UUID IS NOT SET! Inventory features may not work!"); } return new InventoryContextImpl(resource, this); } private void processSyncInfo(Collection<ResourceSyncInfo> syncInfos, Set<Resource> syncedResources, Set<ResourceSyncInfo> unknownResourceSyncInfos, Set<Integer> modifiedResourceIds, Set<Resource> newlyCommittedResources, Set<Resource> ignoredResources) { for (ResourceSyncInfo syncInfo : syncInfos) { ResourceContainer container = getResourceContainer(syncInfo.getUuid()); if (container == null) { // Either a manually added Resource or just something we haven't discovered. // If this unknown resource is to be ignored, then don't bother to do anything. if (InventoryStatus.IGNORED != syncInfo.getInventoryStatus()) { unknownResourceSyncInfos.add(syncInfo); } else { log.info("Got an unknown but ignored resource - ignoring it: " + syncInfo.getId()); } } else { Resource resource = container.getResource(); // Ensure the Resource classloader is initialized on the Resource container. initResourceContainer(resource); if (log.isDebugEnabled()) { log.debug("Local Resource: id=" + resource.getId() + ", status=" + resource.getInventoryStatus() + ", mtime=" + resource.getMtime()); log.debug("Sync Resource: " + syncInfo.getId() + ", status=" + syncInfo.getInventoryStatus() + ", mtime=" + syncInfo.getMtime()); } final boolean ignoreResource = (InventoryStatus.IGNORED == syncInfo.getInventoryStatus()); final boolean ignoreResourceType = this.pluginManager.getMetadataManager() .isDisabledOrIgnoredResourceType(resource.getResourceType()); if (ignoreResource || ignoreResourceType) { // a resource or its type has been tagged to be ignored - we need to remove it from our inventory ignoredResources.add(resource); } else { if (resource.getInventoryStatus() != InventoryStatus.COMMITTED && syncInfo.getInventoryStatus() == InventoryStatus.COMMITTED) { newlyCommittedResources.add(resource); } if (resource.getId() == 0) { // This must be a Resource we just reported to the server. Just update its id, mtime, and status. resource.setId(syncInfo.getId()); resource.setMtime(syncInfo.getMtime()); resource.setInventoryStatus(syncInfo.getInventoryStatus()); refreshResourceComponentState(container, true); syncedResources.add(resource); } else { // It's a resource that was already synced at least once. if (resource.getId() != syncInfo.getId()) { // This really should never happen, but check for it just to be bulletproof. log.error("PC Resource id (" + resource.getId() + ") does not match Server Resource id (" + syncInfo.getId() + ") for Resource with uuid " + resource.getUuid() + ": " + resource); modifiedResourceIds.add(syncInfo.getId()); } // See if it's been modified on the Server since the last time we synced. else if (resource.getMtime() < syncInfo.getMtime()) { modifiedResourceIds.add(resource.getId()); } else { // Only try to start up the component if the Resource has *not* been modified on the Server. // Otherwise, hold off until we've synced the Resource with the Server. refreshResourceComponentState(container, false); } } } } } } private void compactResource(final Resource resource) { resource.setAncestry(null); resource.setAlertDefinitions(null); resource.setLocation(null); resource.setModifiedBy(null); resource.setOperationHistories(Collections.EMPTY_LIST); resource.setTags(Collections.EMPTY_SET); resource.setInstalledPackages(Collections.EMPTY_SET); resource.setInstalledPackageHistory(Collections.EMPTY_LIST); resource.setDescription(null); resource.setImplicitGroups(Collections.EMPTY_SET); resource.setExplicitGroups(Collections.EMPTY_SET); resource.setCreateChildResourceRequests(Collections.EMPTY_LIST); resource.setAutoGroupBackingGroups(Collections.EMPTY_LIST); if (resource.getSchedules() != null) { // TODO used at all in the agent? if (resource.getSchedules().size() == 0) { resource.setSchedules(Collections.EMPTY_SET); } } if (resource.getVersion() != null) { resource.setVersion(resource.getVersion().intern()); } if (resource.getName() != null) { resource.setName(resource.getName().intern()); } Configuration pluginConfiguration = resource.getPluginConfiguration(); if (pluginConfiguration != null) { pluginConfiguration.cleanoutRawConfiguration(); compactConfiguration(pluginConfiguration); } Configuration resourceConfiguration = resource.getResourceConfiguration(); if (resourceConfiguration != null) { resourceConfiguration.cleanoutRawConfiguration(); boolean persisted = ConfigurationCheckExecutor.persistConfigurationToFile(this, resource.getId(), resourceConfiguration, log); if (persisted) { resource.setResourceConfiguration(null); } } } public static Configuration getResourceConfiguration(Resource agentSideResource) { return ConfigurationCheckExecutor.getResourceConfiguration(PluginContainer.getInstance().getInventoryManager(), agentSideResource); } private void compactConfiguration(Configuration config) { if (config == null) { return; } if (config.getProperties() == null) { return; } for (Property prop : config.getProperties()) { if (prop.getName() != null) { prop.setName(prop.getName().intern()); } } config.resize(); } private Set<Resource> mergeModifiedResources(Set<Integer> modifiedResourceIds) { if (null == modifiedResourceIds || modifiedResourceIds.isEmpty()) { return Collections.emptySet(); } if (log.isDebugEnabled()) { log.debug("Merging [" + modifiedResourceIds.size() + "] modified Resources into local inventory..."); } Set<Resource> modifiedResources = configuration.getServerServices().getDiscoveryServerService() .getResources(modifiedResourceIds, false); syncSchedules(modifiedResources); // RHQ-792, mtime is the indicator that schedules should be sync'ed too syncDriftDefinitions(modifiedResources); for (Resource modifiedResource : modifiedResources) { compactResource(modifiedResource); mergeResource(modifiedResource); } return modifiedResources; } private Set<Resource> mergeUnknownResources(Set<ResourceSyncInfo> unknownResourceSyncInfos) { if (null == unknownResourceSyncInfos || unknownResourceSyncInfos.isEmpty()) { return Collections.emptySet(); } if (log.isDebugEnabled()) { log.debug("Retrieving unknown Resources for [" + unknownResourceSyncInfos.size() + "] unknownResourceSyncInfos..."); } PluginMetadataManager pmm = this.pluginManager.getMetadataManager(); Set<Resource> unknownResources = getResourcesFromSyncInfos(unknownResourceSyncInfos); if (log.isDebugEnabled()) { log.debug("Merging [" + unknownResources.size() + "] unknown Resources (and descendents) into inventory..."); } for (Resource unknownResource : unknownResources) { ResourceType resourceType = pmm.getType(unknownResource.getResourceType()); if (resourceType != null) { log.info("Got unknown resource: " + unknownResource.getId()); compactResource(unknownResource); mergeResource(unknownResource); syncSchedulesRecursively(unknownResource); syncDriftDefinitionsRecursively(unknownResource); } else { if (log.isDebugEnabled()) { log.debug("During an inventory sync, the server gave us Resource [" + unknownResource + "] but its type is disabled in the agent; skipping it..."); } // report this so that the user knows to uninventory the dead resource String msg = "This resource should be uninventoried. The agent has disabled this resource's type. The resource is no longer being managed."; ResourceError resourceError = new ResourceError(unknownResource, ResourceErrorType.DISABLED_TYPE, msg, msg, System.currentTimeMillis()); sendResourceErrorToServer(resourceError); } } return unknownResources; } private Set<Resource> getResourcesFromSyncInfos(Set<ResourceSyncInfo> syncInfos) { ///// // First we need to get a list of the unknown resource ids List<Integer> resourceIdList = new ArrayList<Integer>(syncInfos.size()); for (ResourceSyncInfo syncInfo : syncInfos) { resourceIdList.add(syncInfo.getId()); } ///// // Now we need to loop over batches of the resource ID list - asking the server for their resource representations. // When we get the resources from the server, we put them in our resourceMap, keyed on ID. final boolean isDebugEnabled = log.isDebugEnabled(); final StopWatch stopWatch = new StopWatch(); String marker = null; Map<Integer, Resource> resourceMap = new HashMap<Integer, Resource>(resourceIdList.size()); int batchNumber = 0; while (!resourceIdList.isEmpty()) { // Our current batch starts at the head of the list, but // we need to determine how big our current batch should be and where in the list of IDs that batch ends int size = resourceIdList.size(); int end = (SYNC_BATCH_SIZE < size) ? SYNC_BATCH_SIZE : size; batchNumber++; // Determine the content of our current batch - this is simply a sublist of our IDs list. // Note that we use .clear() once we get the batch array in order to advance our progress and help GC. // This usage of .clear() will remove the processed resources from the backing list. String markerPrefix = null; if (isDebugEnabled) { markerPrefix = String.format("a. Batch [%03d] (%d): ", batchNumber, syncInfos.size()); marker = String.format("%sGet resource ID sublist - %d of %d remaining", markerPrefix, end, size); stopWatch.markTimeBegin(marker); } List<Integer> resourceIdBatch = resourceIdList.subList(0, end); Integer[] resourceIdArray = resourceIdBatch.toArray(new Integer[resourceIdBatch.size()]); resourceIdBatch.clear(); if (isDebugEnabled) { stopWatch.markTimeEnd(marker); marker = markerPrefix + "Get sublist of resources from server"; stopWatch.markTimeBegin(marker); } // Ask the server for the resource representation of all resource IDs in our batch. // This is a potentially expensive operation depending on the size of the batch and the content of the resources. List<Resource> resourceBatch = configuration.getServerServices().getDiscoveryServerService() .getResourcesAsList(resourceIdArray); if (isDebugEnabled) { stopWatch.markTimeEnd(marker); marker = markerPrefix + "Store sublist of resources to map"; stopWatch.markTimeBegin(marker); } // Now that the server told us the resources in our batch, we add them to our master map. // Note our usage of clear on the batch - this is to help GC. for (Resource r : resourceBatch) { // protect against childResources notNull assumptions downstream if (null == r.getChildResources()) { r.setChildResources(new CopyOnWriteArraySet()); // this will actually initialize to an empty Set } compactResource(r); resourceMap.put(r.getId(), r); } resourceBatch.clear(); if (isDebugEnabled) { stopWatch.markTimeEnd(marker); } } if (syncInfos.size() != resourceMap.size()) { log.warn("Expected [" + syncInfos.size() + "] but found [" + resourceMap.size() + "] resources when fetching from server"); } ///// // We now have all the resources associated with all sync infos in a map. // We need to build the full resource tree using the resource parent info as the blueprint for how to // link the resources in the tree. if (isDebugEnabled) { marker = "b. Build the resource hierarchies"; stopWatch.markTimeBegin(marker); } // The root resources to be merged (i.e. the resources whose parents are not in the map) Set<Resource> result = new HashSet<Resource>(); for (Resource resource : resourceMap.values()) { if (null == resource.getParentResource()) { result.add(resource); // the platform, make sure we have this continue; } Resource parent = resourceMap.get(resource.getParentResource().getId()); if (null != parent) { parent.addChildResource(resource); } else { result.add(resource); } } if (isDebugEnabled) { stopWatch.markTimeEnd(marker); log.debug("Resource trees built from map - performance: " + stopWatch); } return result; } // private void print(Resource resourceTreeNode, int level) { // StringBuilder builder = new StringBuilder(); // for (int i = 0; i < level; i++) { // builder.append(" "); // } // log.info(builder.toString() + resourceTreeNode.getId() + " " + resourceTreeNode.getUuid()); // for (Resource child : resourceTreeNode.getChildResources()) { // print(child, level + 1); // } // } private void mergeResource(Resource resourceFromServer) { if (log.isDebugEnabled()) { log.debug("Merging " + resourceFromServer + " into local inventory..."); } // Replace the stripped-down ResourceType that came from the Server with the full ResourceType - it's critical // to do this before merging the Resource, because the plugin container and plugins rely on the type being fully // initialized. if (!hydrateResourceType(resourceFromServer)) { return; } // Find the Resource's parent in our inventory. Resource parentResource; Resource parentResourceFromServer = resourceFromServer.getParentResource(); if (parentResourceFromServer != null) { ResourceContainer parentResourceContainer = getResourceContainer(parentResourceFromServer); if (parentResourceContainer == null) { // must get the resContainer via Id here, the uuid is not necessarily set in the parent parentResourceContainer = getResourceContainer(parentResourceFromServer.getId()); } if (parentResourceContainer != null) { parentResource = parentResourceContainer.getResource(); } else { log.debug("Skipping merge of " + resourceFromServer + " into local inventory, since a container was not found for its parent " + parentResourceFromServer + "."); return; } } else { // A null parent means this is the platform. parentResource = null; } // See if the Resource already exists in our inventory. Resource existingResource = findMatchingChildResource(resourceFromServer, parentResource); if ((existingResource == null) && (resourceFromServer.getResourceType().getCategory() == ResourceCategory.PLATFORM)) { // This should never happen, but add a check so we'll know if it ever does. log.error("Existing platform [" + this.platform + "] has different Resource type and/or Resource key than " + "platform in Server inventory: " + resourceFromServer); } boolean pluginConfigUpdated; Resource mergedResource; ResourceContainer resourceContainer; this.inventoryLock.writeLock().lock(); try { if (existingResource != null) { // modified Resource log.debug("Modifying " + existingResource + " in local inventory - Resource from Server is " + resourceFromServer + "."); // First grab the existing Resource's container, so we can reuse it. resourceContainer = this.resourceContainersByUUID.remove(existingResource.getUuid()); if (resourceContainer != null) { this.resourceContainerByResourceId.remove(existingResource.getId()); this.resourceContainersByUUID.put(resourceFromServer.getUuid().intern(), resourceContainer); this.resourceContainerByResourceId.put(resourceFromServer.getId(), resourceContainer); } else { log.error("No ResourceContainer found for existing " + existingResource + "."); return; } if (parentResource != null) { // It's critical to remove the existing Resource from the parent's child Set if the UUID has // changed (i.e. altering the hashCode of an item in a Set == BAD), so just always remove it. parentResource.removeChildResource(existingResource); } // Now merge the Resource from the Server into the existing Resource... pluginConfigUpdated = mergeResource(resourceFromServer, existingResource); mergedResource = existingResource; } else { // unknown Resource log.debug("Adding unknown " + resourceFromServer + " to local inventory."); pluginConfigUpdated = false; mergedResource = cloneResourceWithoutChildren(resourceFromServer); } if (parentResource != null) { parentResource.addChildResourceWithoutAncestry(mergedResource); } else { this.platform = mergedResource; } resourceContainer = initResourceContainer(mergedResource); } finally { this.inventoryLock.writeLock().unlock(); } refreshResourceComponentState(resourceContainer, pluginConfigUpdated); // Recursively merge the children. Note - don't recurse using containers, we're merging the // the provided hierarchy not traversing the existing hierarchy for (Resource childResource : resourceFromServer.getChildResources()) { mergeResource(childResource); } } private boolean hydrateResourceType(Resource resourceFromServer) { ResourceType fullResourceType = this.pluginManager.getMetadataManager().getType( resourceFromServer.getResourceType()); if (fullResourceType == null) { log.error(resourceFromServer + " being synced from Server has an unknown type [" + resourceFromServer.getResourceType() + "] - the [" + resourceFromServer.getResourceType().getPlugin() + "] plugin is most likely not up to date in the Agent - try updating the Agent's plugins."); return false; } resourceFromServer.setResourceType(fullResourceType); return true; } private boolean mergeResource(Resource sourceResource, Resource targetResource) { if (targetResource.getId() != 0 && targetResource.getId() != sourceResource.getId()) { log.warn("Id for " + targetResource + " changed from [" + targetResource.getId() + "] to [" + sourceResource.getId() + "]."); } targetResource.setId(sourceResource.getId()); targetResource.setUuid(sourceResource.getUuid()); if (!targetResource.getResourceKey().equals(sourceResource.getResourceKey())) { log.warn("Resource key for " + targetResource + " changed from [" + targetResource.getResourceKey() + "] to [" + sourceResource.getResourceKey() + "]."); } targetResource.setResourceKey(sourceResource.getResourceKey()); targetResource.setResourceType(sourceResource.getResourceType()); targetResource.setMtime(sourceResource.getMtime()); targetResource.setInventoryStatus(sourceResource.getInventoryStatus()); // (jshaughn) noticed we don't set the version here, should we? boolean pluginConfigUpdated = (!targetResource.getPluginConfiguration().equals( sourceResource.getPluginConfiguration())); targetResource.setPluginConfiguration(sourceResource.getPluginConfiguration()); targetResource.setName(sourceResource.getName()); compactResource(targetResource); return pluginConfigUpdated; } private void purgeIgnoredResources(Set<Resource> ignoredResources) { if (ignoredResources == null || ignoredResources.isEmpty()) { return; } log.debug("Purging [" + ignoredResources.size() + "] ignored resources..."); this.inventoryLock.writeLock().lock(); try { for (Resource ignoredResource : ignoredResources) { uninventoryResource(ignoredResource.getId()); } } finally { this.inventoryLock.writeLock().unlock(); } } private void purgeObsoleteResources(Set<String> allUuids) { // Remove previously synchronized Resources that no longer exist in the Server's inventory... log.debug("Purging obsolete Resources..."); this.inventoryLock.writeLock().lock(); try { int removedResources = 0; /* * use a separate map for iterating, so that later calls to resourceContainers.remove(ResourceContainer) * can be called later without throwing ConcurrentModificationException */ Map<String, ResourceContainer> mapForIterating = new HashMap<String, ResourceContainer>( this.resourceContainersByUUID); for (String uuid : mapForIterating.keySet()) { if (!allUuids.contains(uuid)) { ResourceContainer resourceContainer = this.resourceContainersByUUID.get(uuid); if (resourceContainer != null) { Resource resource = resourceContainer.getResource(); // Only purge stuff that was synchronized at some point. Other stuff may just be newly discovered. if (resource.getId() != 0) { uninventoryResource(resource.getId()); removedResources++; } } else { log.debug("No obsolete resource to purge - no container for uuid: " + uuid); } } } if (log.isDebugEnabled()) { log.debug("Purged [" + removedResources + "] obsolete Resources."); } } finally { this.inventoryLock.writeLock().unlock(); } } private void refreshResourceComponentState(ResourceContainer container, boolean pluginConfigUpdated) { if (isResourceUpgradeActive()) { //don't do anything during upgrade. The resources are only started during the upgrade process. return; } Resource resource = container.getResource(); switch (resource.getInventoryStatus()) { case COMMITTED: { try { if (pluginConfigUpdated) { deactivateResource(resource); } activateResource(resource, container, pluginConfigUpdated); } catch (InvalidPluginConfigurationException ipce) { handleInvalidPluginConfigurationResourceError(resource, ipce); log.warn("Cannot start component for " + resource + " from synchronized merge due to invalid plugin config: " + ipce.getLocalizedMessage()); } catch (Exception e) { log.error("Failed to start component for " + resource + " from synchronized merge.", e); } break; } default: // nothing to do for other states break; } container.setSynchronizationState(ResourceContainer.SynchronizationState.SYNCHRONIZED); } private void activateAndUpgradeResources() { try { log.info("Starting resource activation and upgrade."); long start = System.currentTimeMillis(); log.info("Executing the initial inventory synchronization before upgrade."); boolean syncResult = handleReport(new InventoryReport(getAgent())); if (!syncResult) { log.warn("Failed to sync up the inventory with the server. The resource upgrade will be disabled."); } log.info("Starting to activate (and upgrade) resources."); activateAndUpgradeResourceRecursively(getPlatform(), syncResult); log.info("Inventory activated and upgrade requests gathered in " + (System.currentTimeMillis() - start) + "ms."); log.info("Sending the upgrade requests to the server."); resourceUpgradeDelegate.sendRequests(); log.info("Resource activation and upgrade finished."); } catch (Throwable t) { log.error( "Resource activation or upgrade failed with an exception. An attempt to merely activate the resources will be made now.", t); //make sure to at least activate the resources activateAndUpgradeResourceRecursively(getPlatform(), false); } finally { resourceUpgradeDelegate.disable(); } } private void activateAndUpgradeResourceRecursively(Resource resource, boolean doUpgrade) { ResourceContainer container = initResourceContainer(resource); boolean activate = true; //only do upgrade inside the agent. it does not make sense in embedded mode. if (doUpgrade && configuration.isInsideAgent()) { try { activate = prepareResourceForActivation(resource, container, false); activate = activate && resourceUpgradeDelegate.processAndQueue(container); } catch (Throwable t) { log.error("Exception thrown while upgrading [" + resource + "].", t); activate = false; } } if (activate) { try { activateResource(resource, container, false); for (Resource child : getContainerChildren(resource, container)) { activateAndUpgradeResourceRecursively(child, doUpgrade); } } catch (InvalidPluginConfigurationException e) { log.debug("Failed to activate resource [" + resource + "] due to invalid plugin configuration.", e); handleInvalidPluginConfigurationResourceError(resource, e); } catch (Throwable t) { log.error("Exception thrown while activating [" + resource + "].", t); handleInvalidPluginConfigurationResourceError(resource, t); } } } /** * That class implements a listener that gets called when the resource got activated * @author hrupp * */ class ResourceGotActivatedListener implements InventoryEventListener { @Override public void resourceActivated(Resource resource) { if (resource != null && resource.getId() > 0) { if (log.isDebugEnabled()) { log.debug("Resource got finally activated, cleaning out config errors: " + resource); } DiscoveryServerService serverService = configuration.getServerServices().getDiscoveryServerService(); if (serverService != null) { serverService.clearResourceConfigError(resource.getId()); } } removeInventoryEventListener(this); } @Override public void resourceDeactivated(Resource resource) { // nothing to do } @Override public void resourcesAdded(Set<Resource> resources) { // nothing to do } @Override public void resourcesRemoved(Set<Resource> resources) { // nothing to do } } public PluginComponentFactory getPluginComponentFactory() { return this.pluginFactory; } public PluginManager getPluginManager() { return pluginManager; } public EventManager getEventManager() { return eventManager; } public OperationManager getOperationManager() { return operationManager; } public MeasurementManager getMeasurementManager() { return measurementManager; } public ContentManager getContentManager() { return contentManager; } }