/*
* 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 java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.clientapi.agent.metadata.ResourceTypeNotEnabledException;
import org.rhq.core.clientapi.server.discovery.InventoryReport;
import org.rhq.core.domain.measurement.Availability;
import org.rhq.core.domain.measurement.AvailabilityType;
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.ResourceType;
import org.rhq.core.pc.PluginContainerConfiguration;
import org.rhq.core.pc.plugin.PluginComponentFactory;
import org.rhq.core.pluginapi.availability.AvailabilityFacet;
import org.rhq.core.pluginapi.inventory.ProcessScanResult;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent;
import org.rhq.core.util.exception.ExceptionPackage;
import org.rhq.core.util.exception.Severity;
/**
* This should probably be renamed to ServiceDiscoveryExecutor or maybe ChildDiscoveryExecutor. It is responsible for
* discovering children of existing resources. It recursively walks the hierarchy looking for new resources, which
* are typically services (but could be non-top-level servers). It is complemented by {@link AutoDiscoveryExecutor}
* which looks for new top level servers.
*
* @author Greg Hinkle
* @author Ian Springer
*/
public class RuntimeDiscoveryExecutor implements Runnable, Callable<InventoryReport> {
private static final Log log = LogFactory.getLog(RuntimeDiscoveryExecutor.class);
private final InventoryManager inventoryManager;
private final PluginContainerConfiguration pluginContainerConfiguration;
/**
* Resource to scan. If null, the entire platform will be scanned.
*/
private Resource rootResource;
public RuntimeDiscoveryExecutor(InventoryManager inventoryManager,
PluginContainerConfiguration pluginContainerConfiguration) {
this.inventoryManager = inventoryManager;
this.pluginContainerConfiguration = pluginContainerConfiguration;
}
/**
* Creates a new <code>RuntimeDiscoveryExecutor</code> instance that will run a discovery scoped to the specified
* agent.
*
* @param inventoryManager hook back to the inventory manager
* @param pluginContainerConfiguration configuration of this executor
* @param rootResource scopes the runtime scan to a particular resource
*/
public RuntimeDiscoveryExecutor(InventoryManager inventoryManager,
PluginContainerConfiguration pluginContainerConfiguration, Resource rootResource) {
this(inventoryManager, pluginContainerConfiguration);
this.rootResource = rootResource;
}
public void run() {
call();
}
@NotNull
public InventoryReport call() {
String target = (rootResource != null) ? this.rootResource.toString() : "platform";
log.info("Executing runtime discovery scan rooted at [" + target + "]...");
InventoryReport report = new InventoryReport(inventoryManager.getAgent());
try {
report.setRuntimeReport(true);
report.setStartTime(System.currentTimeMillis());
runtimeDiscover(report);
report.setEndTime(System.currentTimeMillis());
if (log.isDebugEnabled()) {
log.debug(String.format("Runtime discovery scan took %d ms.",
(report.getEndTime() - report.getStartTime())));
}
// TODO: This is always zero for embedded because we don't populate the report.
int numAddedRoots = report.getAddedRoots().size();
int numNewDescendants = (report.getResourceCount() - numAddedRoots);
log.info("Scanned " + target + " and " + numAddedRoots + " server(s) and discovered " + numNewDescendants
+ " new descendant Resource(s).");
// TODO GH: This is principally valuable only until we work out the last of the data transfer situations.
if (log.isTraceEnabled()) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(report);
log.trace("Runtime report contains " + report.getResourceCount() + " Resources with a size of "
+ baos.size() + " bytes");
}
// If no child resources were discovered then don't send the empty report. This avoids some
// major overhead in that each each report sent triggers a full inventory sync.
if (numNewDescendants > 0) {
this.inventoryManager.handleReport(report);
} else {
log.info("Not sending inventory report because no new descendent resources were discovered.");
}
} catch (Exception e) {
log.warn("Exception caught while executing runtime discovery scan rooted at [" + target + "].", e);
report.addError(new ExceptionPackage(Severity.Warning, e));
}
return report;
}
private void runtimeDiscover(InventoryReport report) throws PluginContainerException {
try {
if (this.rootResource == null) {
// Run a full scan for all resources in the inventory
Resource platform = this.inventoryManager.getPlatform();
// Discover platform services here
discoverForResource(platform, report, false);
} else {
// Run a single scan for just a resource and its descendants
discoverForResource(rootResource, report, false);
}
} catch (InterruptedException e) {
log.info("Service discovery interrupted. This is OK and typically due to new request for service scan. Returning results collected up to this point.");
}
return;
}
/**
* @param parent The parent resource to look for children of
* @param report The report to add the resource to
* @param parentReported true if the resources parent is already in the inventory report and therefore will include
* this resource and its descendants in the report under that root
*
* @throws PluginContainerException on error
*/
private void discoverForResource(Resource parent, InventoryReport report, boolean parentReported)
throws PluginContainerException, InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException("Job canceled, stopping service discovery and reporting partial results.");
}
log.debug("Discovering child Resources for " + parent + "...");
ResourceContainer parentContainer = this.inventoryManager.getResourceContainer(parent);
if (parentContainer == null) {
if (log.isDebugEnabled()) {
log.debug("Cannot perform service scan on parent [" + parent + "] without a container");
}
return;
}
if (parentContainer.getResourceComponentState() != ResourceContainer.ResourceComponentState.STARTED) {
if (log.isTraceEnabled()) {
log.trace("Parent [" + parent + "] is not STARTED - not performing service scan");
}
return;
}
if (parent.getInventoryStatus() != InventoryStatus.COMMITTED) {
if (log.isDebugEnabled()) {
log.debug("Parent [" + parent + "] must be imported/committed before service scan can run.");
}
return;
}
// For each child resource type of the server, do a discovery for resources of that type
Set<ResourceType> childResourceTypes = parent.getResourceType().getChildResourceTypes();
if (null == childResourceTypes || childResourceTypes.isEmpty()) {
// I'm not sure it's possible, but just in case, make sure it doesn't have children. If it does, keep going
if (parent.getChildResources().isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Parent resource type [" + parent + "] has no child types; cannot perform service scan.");
}
return;
}
}
// At this point we used to always do a live check of availability. This buys us very little and
// costs us a lot. For a platform-rooted scan this ends up being an avail check for all but leaf
// nodes of the tree. That is costly on top of the discovery check itself, and is antithetical to
// the whole staggered avail-check approach we now have in place. We can't even update the container
// with the avail check result, because all changes in avail need to be detected and reported by the
// AvailabilityExecutor. We did the avail check for two reasons. If there is no current avail we
// need to establish one because it may be for a resource newly discovered by this scan, and we need to know
// if we can in turn perform discovery on it. We still need to do this check. The second was to perform
// discovery only on UP resources. We can keep this logic but just use the current availability
// stored in the container. It may be stale, but it is likely valid, as avail does not often change.
// An argument could be made to always use the currently stored avail, but currently we've decided
// to still perform the check in two cases: if the current avail is not UP or if the resource category is
// SERVER. This means we won't miss an opportunity to do discovery for stale DOWN resource, and we won't
// waste time doing discovery on a stale UP SERVER, which can be time consuming. Since most resources are
// SERVICEs, and also are typically UP and stay UP, performing checks in these two situations should
// not add much overhead. Finally, make sure to use facet proxy to do the avail check, this allows us to use
// a timeout, and therefore not hang discovery if the avail check is slow.
Availability currentAvailability = parentContainer.getAvailability();
AvailabilityType currentAvailabilityType = (null == currentAvailability) ? AvailabilityType.DOWN
: currentAvailability.getAvailabilityType();
// If there is no current avail, or this is a SERVER, we must perform the live check.
if (AvailabilityType.UP != currentAvailabilityType
|| ResourceCategory.SERVER == parentContainer.getResource().getResourceType().getCategory()) {
AvailabilityFacet parentAvailabilityProxy = parentContainer.getAvailabilityProxy();
try {
currentAvailabilityType = parentAvailabilityProxy.getAvailability();
} catch (Exception e) {
currentAvailabilityType = AvailabilityType.DOWN;
}
}
if (AvailabilityType.UP != currentAvailabilityType) {
if (log.isDebugEnabled()) {
log.debug("Availability of [" + parent + "] is not UP, cannot perform service scan on it.");
}
return;
}
PluginComponentFactory factory = inventoryManager.getPluginComponentFactory();
try {
for (ResourceType childResourceType : childResourceTypes) {
// Make sure we have a discovery component for that type, otherwise there is nothing to do
ResourceDiscoveryComponent discoveryComponent = null;
try {
discoveryComponent = factory.getDiscoveryComponent(childResourceType, parentContainer);
} catch (ResourceTypeNotEnabledException rtne) {
if (log.isDebugEnabled()) {
log.debug("Resource not discoverable, type is disabled: " + childResourceType);
}
continue; // do not discovery anything for this component
} catch (PluginContainerException pce) {
log.error("Unable to obtain discovery component for [" + childResourceType + "]", pce);
}
if (discoveryComponent == null) {
if (log.isDebugEnabled()) {
log.debug("Resource not discoverable, no component found: " + childResourceType);
}
continue; // Assume its not discoverable
}
// For this resource type, discover all resources of that type on this parent resource
if (log.isDebugEnabled()) {
log.debug("Running service scan on parent resource [" + parent + "] looking for children of type ["
+ childResourceType + "]");
}
Set<Resource> discoveredChildResources = this.inventoryManager
.executeComponentDiscovery(childResourceType, discoveryComponent, parentContainer,
Collections.<ProcessScanResult> emptyList());
// For each discovered child resource, update it in the inventory manager
Map<String, Resource> mergedResources = new HashMap<String, Resource>();
for (Resource discoveredChildResource : discoveredChildResources) {
Resource mergedResource;
mergedResource = this.inventoryManager.mergeResourceFromDiscovery(discoveredChildResource, parent);
mergedResources.put(mergedResource.getUuid(), mergedResource);
if ((mergedResource.getId() == 0) && !parentReported) {
report.addAddedRoot(parent);
parentReported = true;
}
}
// get rid of any child resources of this type that were not yet committed and are now gone
removeStaleResources(parent, childResourceType, mergedResources);
}
// now, recursively perform discovery on all of the parent's children, which includes the newly
// merged children as well as previously existing children.
for (Resource childResource : parent.getChildResources()) {
discoverForResource(childResource, report, parentReported);
}
} catch (InterruptedException e) {
throw e; // if we're interrupted then exit out
} catch (Throwable t) {
report.getErrors().add(new ExceptionPackage(Severity.Severe, t));
log.error("Error in runtime discovery", t);
}
return;
}
// TODO: Move this to InventoryManager, so it can be used by AutoDiscoveryExecutor too.
private void removeStaleResources(Resource parent, ResourceType childResourceType,
Map<String, Resource> mergedResources) {
Set<Resource> existingChildResources = parent.getChildResources();
for (Resource existingChildResource : existingChildResources) {
// NOTE: If inside Agent, only remove Resources w/ id == 0. Other Resources may still exist in the
// the Server's inventory.
if (existingChildResource.getResourceType().equals(childResourceType)
&& !mergedResources.containsKey(existingChildResource.getUuid())
&& (existingChildResource.getId() == 0 || !this.pluginContainerConfiguration.isInsideAgent())) {
log.info("Removing stale resource [" + existingChildResource + "]");
this.inventoryManager.removeResourceAndIndicateIfScanIsNeeded(existingChildResource, true);
}
}
}
}