/*
* 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.enterprise.server.discovery;
import static org.rhq.core.domain.resource.CreateResourceStatus.SUCCESS;
import static org.rhq.core.domain.util.PageOrdering.DESC;
import static org.rhq.core.util.StringUtil.isBlank;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.security.auth.login.LoginException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.jboss.remoting.CannotConnectException;
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.upgrade.ResourceUpgradeRequest;
import org.rhq.core.clientapi.agent.upgrade.ResourceUpgradeResponse;
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.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.authz.Permission;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PluginConfigurationUpdate;
import org.rhq.core.domain.criteria.ResourceCriteria;
import org.rhq.core.domain.criteria.ResourceTypeCriteria;
import org.rhq.core.domain.discovery.MergeInventoryReportResults;
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.resource.Agent;
import org.rhq.core.domain.resource.CannotConnectToAgentException;
import org.rhq.core.domain.resource.CreateResourceHistory;
import org.rhq.core.domain.resource.ImportResourceRequest;
import org.rhq.core.domain.resource.ImportResourceResponse;
import org.rhq.core.domain.resource.InventoryStatus;
import org.rhq.core.domain.resource.ProductVersion;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceCategory;
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.server.PersistenceUtility;
import org.rhq.core.domain.util.OrderingField;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.domain.util.PageOrdering;
import org.rhq.core.util.collection.ArrayUtils;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.agentclient.AgentClient;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.authz.AuthorizationManagerLocal;
import org.rhq.enterprise.server.authz.PermissionException;
import org.rhq.enterprise.server.authz.RequiredPermission;
import org.rhq.enterprise.server.cloud.StorageNodeManagerLocal;
import org.rhq.enterprise.server.configuration.ConfigurationManagerLocal;
import org.rhq.enterprise.server.core.AgentManagerLocal;
import org.rhq.enterprise.server.measurement.AvailabilityManagerLocal;
import org.rhq.enterprise.server.resource.ProductVersionManagerLocal;
import org.rhq.enterprise.server.resource.ResourceAlreadyExistsException;
import org.rhq.enterprise.server.resource.ResourceAvailabilityManagerLocal;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.resource.ResourceTypeManagerLocal;
import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal;
import org.rhq.enterprise.server.resource.metadata.PluginManagerLocal;
import org.rhq.enterprise.server.system.SystemManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
/**
* SLSB that provides the interface point to the discovery subsystem for the UI layer and the discovery server service.
*
* @author Ian Springer
* @author Greg Hinkle
* @author Jay Shaughnessy
*/
@Stateless
public class DiscoveryBossBean implements DiscoveryBossLocal, DiscoveryBossRemote {
private static final Log LOG = LogFactory.getLog(DiscoveryBossBean.class.getName());
static private final int MERGE_BATCH_SIZE;
static {
int mergeBatchSize = 200;
try {
mergeBatchSize = Integer.parseInt(System.getProperty("rhq.server.discovery.merge.batch.size", "200"));
} catch (Throwable t) {
//
}
MERGE_BATCH_SIZE = mergeBatchSize;
}
@PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
private EntityManager entityManager;
@EJB
private AgentManagerLocal agentManager;
@EJB
private AuthorizationManagerLocal authorizationManager;
@EJB
private DiscoveryBossLocal discoveryBoss; // ourselves for Tx purposes
@EJB
private ResourceGroupManagerLocal groupManager;
@EJB
private ResourceManagerLocal resourceManager;
@EJB
private ResourceAvailabilityManagerLocal resourceAvailabilityManager;
@EJB
private ResourceTypeManagerLocal resourceTypeManager;
@EJB
private SubjectManagerLocal subjectManager;
@EJB
private ProductVersionManagerLocal productVersionManager;
@EJB
private SystemManagerLocal systemManager;
@EJB
private PluginManagerLocal pluginManager;
@EJB
private AvailabilityManagerLocal availabilityManager;
@EJB
private StorageNodeManagerLocal storageNodeManager;
@EJB
private ConfigurationManagerLocal configurationManager;
// Do not start in a transaction. A single transaction may timeout if the report size is too large
@Override
@TransactionAttribute(TransactionAttributeType.NEVER)
public MergeInventoryReportResults mergeInventoryReport(InventoryReport report)
throws InvalidInventoryReportException {
validateInventoryReport(report);
DeletedResourceTypeFilter filter = new DeletedResourceTypeFilter(subjectManager, resourceTypeManager,
pluginManager);
Set<ResourceType> deletedTypes = filter.apply(report);
if (!deletedTypes.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("The inventory report from " + report.getAgent() + " with added roots "
+ report.getAddedRoots() + " contains these deleted resource types " + deletedTypes);
} else {
LOG.info("The inventory report from " + report.getAgent() + " contains these deleted resource types "
+ deletedTypes);
}
throw new StaleTypeException("The report contains one or more resource types that have been marked for "
+ "deletion.");
}
Agent agent = report.getAgent();
long start = System.currentTimeMillis();
Agent knownAgent = agentManager.getAgentByName(agent.getName());
if (knownAgent == null) {
throw new InvalidInventoryReportException("Unknown Agent named [" + agent.getName()
+ "] sent an inventory report - that report will be ignored. "
+ "This error is harmless and should stop appearing after a short while if the platform of the agent ["
+ agent.getName() + "] was recently removed from the inventory. In any other case this is a bug.");
}
if (LOG.isDebugEnabled()) {
LOG.debug("Received inventory report from RHQ Agent [" + knownAgent + "]. Number of added roots: "
+ report.getAddedRoots().size());
}
Set<Resource> roots = report.getAddedRoots();
LOG.debug(report);
Map<String, ResourceType> allTypes = new HashMap<String, ResourceType>();
for (Resource root : roots) {
// Make sure all platform, server, and service types are valid. Also, make sure they're fetched - otherwise
// we'll get persistence exceptions when we try to merge OR persist the platform.
long rootStart = System.currentTimeMillis();
if (!initResourceTypes(root, allTypes)) {
LOG.error("Reported resource [" + root + "] has an unknown type [" + root.getResourceType()
+ "]. The Agent [" + knownAgent + "] most likely has a plugin named '"
+ root.getResourceType().getPlugin()
+ "' installed that is not installed on the Server. Resource will be ignored...");
continue;
}
if (Resource.ROOT != root.getParentResource() && Resource.ROOT_ID == root.getParentResource().getId()) {
// This is a root resource. Just set it that way
root.setParentResource(Resource.ROOT);
}
mergeResource(root, knownAgent);
if (LOG.isDebugEnabled()) {
LOG.debug("Root merged: resource/millis=" + root.getName() + '/'
+ (System.currentTimeMillis() - rootStart));
}
}
allTypes = null; // maybe help GC? we don't need this anymore
// Prepare the ResourceSyncInfo tree which contains all the info the PC needs to sync itself up with us.
// The platform can be null in only one scenario.. a brand new agent has connected to the server
// and that agent is currently trying to upgrade its resources. For that it asks us to send down
// the current inventory on the server side. But at this point there isn't any since that very
// agent just registered and is starting up for the very first time and therefore hasn't had
// a chance yet to send us its full inventory report.
PlatformSyncInfo syncInfo = discoveryBoss.getPlatformSyncInfo(knownAgent);
// we need to also tell the agent if there were any ignored types - we must provide the agent with
// ALL types that are ignored, not just for those resources that were in the report
ResourceTypeCriteria ignoredTypesCriteria = new ResourceTypeCriteria();
ignoredTypesCriteria.addFilterIgnored(true);
ignoredTypesCriteria.setPageControl(PageControl.getUnlimitedInstance());
PageList<ResourceType> ignoredTypes = resourceTypeManager.findResourceTypesByCriteria(
subjectManager.getOverlord(), ignoredTypesCriteria);
MergeInventoryReportResults results;
if (syncInfo != null) {
results = new MergeInventoryReportResults(syncInfo, ignoredTypes);
} else {
results = null;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Inventory merge completed in (" + (System.currentTimeMillis() - start) + ")ms");
}
return results;
}
@Override
public PlatformSyncInfo getPlatformSyncInfo(Agent knownAgent) {
Resource platform = resourceManager.getPlatform(knownAgent);
if (null == platform) {
return null;
}
Set<Resource> toplevelServices = new HashSet<Resource>();
Set<Integer> topLevelServerIds = new HashSet<Integer>();
for (Resource platformChild : platform.getChildResources()) {
switch (platformChild.getResourceType().getCategory()) {
case SERVER:
topLevelServerIds.add(platformChild.getId());
break;
case SERVICE:
toplevelServices.add(platformChild);
break;
default:
break;
}
}
ResourceSyncInfo platformSyncInfo = ResourceSyncInfo.buildResourceSyncInfo(platform);
Set<ResourceSyncInfo> topLevelServiceSyncInfo = getToplevelServiceSyncInfo(toplevelServices);
PlatformSyncInfo result = new PlatformSyncInfo(platformSyncInfo, topLevelServiceSyncInfo, topLevelServerIds);
return result;
}
/**
* At the time of writing (4.10) platform top level services don't have children so this will be quick, but
* write it to handle any future children. In general this will still be a relatively small number of resources.
*
* @param topLevelServices
* @return The top level service hierarchy sync info
*/
private Set<ResourceSyncInfo> getToplevelServiceSyncInfo(Set<Resource> topLevelServices) {
Set<ResourceSyncInfo> result = new HashSet<ResourceSyncInfo>(topLevelServices.size());
Set<Integer> topLevelServiceIds = new HashSet<Integer>();
for (Resource topLevelService : topLevelServices) {
result.add(ResourceSyncInfo.buildResourceSyncInfo(topLevelService));
topLevelServiceIds.add(topLevelService.getId());
}
getToplevelServiceSyncInfoHierarchy(topLevelServiceIds, result);
return result;
}
private void getToplevelServiceSyncInfoHierarchy(Set<Integer> parentIds, Set<ResourceSyncInfo> result) {
if (parentIds.isEmpty()) {
return;
}
Query q = entityManager.createNamedQuery(ResourceSyncInfo.QUERY_SERVICE_CHILDREN);
q.setParameter("parentIds", parentIds);
List<ResourceSyncInfo> childSyncInfos = q.getResultList();
if (!childSyncInfos.isEmpty()) {
result.addAll(childSyncInfos);
Set<Integer> childIds = new HashSet<Integer>(childSyncInfos.size());
for (ResourceSyncInfo childSyncInfo : childSyncInfos) {
childIds.add(childSyncInfo.getId());
}
getToplevelServiceSyncInfoHierarchy(childIds, result);
}
}
@SuppressWarnings("unchecked")
@Override
public Collection<ResourceSyncInfo> getResourceSyncInfo(int resourceId) {
// [PERF] this is an expensive query that can return a large collection. But it's faster than the old way of
// letting hibernate grab the whole hierarchy via eager fetch of children...
Query query;
Collection<ResourceSyncInfo> result;
boolean isNative = true;
DatabaseType dbType = DatabaseTypeFactory.getDefaultDatabaseType();
if (DatabaseTypeFactory.isOracle(dbType)) {
query = entityManager.createNativeQuery(ResourceSyncInfo.QUERY_NATIVE_QUERY_TOP_LEVEL_SERVER_ORACLE);
} else if (DatabaseTypeFactory.isPostgres(dbType)) {
query = entityManager.createNativeQuery(ResourceSyncInfo.QUERY_NATIVE_QUERY_TOP_LEVEL_SERVER_POSTGRES);
} else {
isNative = false;
query = entityManager.createNamedQuery(ResourceSyncInfo.QUERY_TOP_LEVEL_SERVER);
}
query.setParameter("resourceId", resourceId);
if (isNative) {
List<Object[]> rows = query.getResultList();
result = new ArrayList<ResourceSyncInfo>(rows.size());
for (Object[] row : rows) {
int id = dbType.getInteger(row[0]);
String uuid = (String) row[1];
long mtime = dbType.getLong(row[2]);
InventoryStatus status = InventoryStatus.valueOf((String) row[3]);
result.add(new ResourceSyncInfo(id, uuid, mtime, status));
}
} else {
result = query.getResultList();
}
return result;
}
@Override
@RequiredPermission(Permission.MANAGE_INVENTORY)
public Map<Resource, List<Resource>> getQueuedPlatformsAndServers(Subject user, PageControl pc) {
return getQueuedPlatformsAndServers(user, EnumSet.of(InventoryStatus.NEW), pc);
}
@Override
@RequiredPermission(Permission.MANAGE_INVENTORY)
public Map<Resource, List<Resource>> getQueuedPlatformsAndServers(Subject user, EnumSet<InventoryStatus> statuses,
PageControl pc) {
// pc.initDefaultOrderingField("res.ctime", PageOrdering.DESC); // this is set in getQueuedPlatforms,
// maps a platform to a list of child servers
Map<Resource, List<Resource>> queuedResources = new HashMap<Resource, List<Resource>>();
List<Resource> queuedPlatforms = getQueuedPlatforms(user, statuses, pc);
for (Resource platform : queuedPlatforms) {
List<Resource> queuedServers = new ArrayList<Resource>();
for (InventoryStatus status : statuses) {
queuedServers.addAll(getQueuedPlatformChildServers(user, status, platform));
}
queuedResources.put(platform, queuedServers);
}
return queuedResources;
}
@Override
@RequiredPermission(Permission.MANAGE_INVENTORY)
@SuppressWarnings("unchecked")
public PageList<Resource> getQueuedPlatforms(Subject user, EnumSet<InventoryStatus> statuses, PageControl pc) {
pc.initDefaultOrderingField("res.ctime", PageOrdering.DESC); // show the newest ones first by default
Query queryCount = PersistenceUtility.createCountQuery(entityManager,
Resource.QUERY_FIND_QUEUED_PLATFORMS_BY_INVENTORY_STATUS);
queryCount.setParameter("inventoryStatuses", statuses);
long count = (Long) queryCount.getSingleResult();
List<Resource> results;
if (count > 0) {
Query query = PersistenceUtility.createQueryWithOrderBy(entityManager,
Resource.QUERY_FIND_QUEUED_PLATFORMS_BY_INVENTORY_STATUS, pc);
query.setParameter("inventoryStatuses", statuses);
results = query.getResultList();
} else
results = Collections.emptyList();
return new PageList<Resource>(results, (int) count, pc);
}
@Override
@RequiredPermission(Permission.MANAGE_INVENTORY)
public List<Resource> getQueuedPlatformChildServers(Subject user, InventoryStatus status, Resource platform) {
PageList<Resource> childServers = resourceManager.findChildResourcesByCategoryAndInventoryStatus(user,
platform, ResourceCategory.SERVER, status, PageControl.getUnlimitedInstance());
return childServers;
}
@Override
@RequiredPermission(Permission.MANAGE_INVENTORY)
public void updateInventoryStatus(Subject user, List<Resource> platforms, List<Resource> servers,
InventoryStatus status) {
long start = System.currentTimeMillis();
// need to attach the resources
List<Resource> attachedPlatforms = new ArrayList<Resource>(platforms.size());
for (Resource p : platforms) {
attachedPlatforms.add(entityManager.find(Resource.class, p.getId()));
}
platforms = attachedPlatforms;
List<Resource> attachedServers = new ArrayList<Resource>(servers.size());
for (Resource s : servers) {
attachedServers.add(entityManager.find(Resource.class, s.getId()));
}
servers = attachedServers;
// Update and persist the actual inventory statuses
// This is done in a separate transaction to stop failures in the agent from rolling back the transaction
discoveryBoss.updateInventoryStatusInNewTransaction(user, platforms, servers, status);
scheduleAgentInventoryOperationJob(platforms, servers);
if (LOG.isDebugEnabled()) {
LOG.debug("Inventory status set to [" + status + "] for [" + platforms.size() + "] platforms and ["
+ servers.size() + "] servers in [" + (System.currentTimeMillis() - start) + "]ms");
}
}
private boolean isJobScheduled(Scheduler scheduler, String name, String group) {
boolean isScheduled = false;
try {
JobDetail jobDetail = scheduler.getJobDetail(name, group);
if (jobDetail != null) {
isScheduled = true;
}
} catch (SchedulerException se) {
LOG.error("Error getting job detail", se);
}
return isScheduled;
}
private void scheduleAgentInventoryOperationJob(List<Resource> platforms, List<Resource> servers) {
Scheduler scheduler = LookupUtil.getSchedulerBean();
try {
final String DEFAULT_JOB_NAME = "AgentInventoryUpdateJob";
final String DEFAULT_JOB_GROUP = "AgentInventoryUpdateGroup";
final String TRIGGER_PREFIX = "AgentInventoryUpdateTrigger";
final String randomSuffix = UUID.randomUUID().toString();
final String triggerName = TRIGGER_PREFIX + " - " + randomSuffix;
SimpleTrigger trigger = new SimpleTrigger(triggerName, DEFAULT_JOB_GROUP, new Date());
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(AgentInventoryStatusUpdateJob.KEY_TRIGGER_NAME, triggerName);
jobDataMap.put(AgentInventoryStatusUpdateJob.KEY_TRIGGER_GROUP_NAME, DEFAULT_JOB_GROUP);
AgentInventoryStatusUpdateJob.externalizeJobValues(jobDataMap,
AgentInventoryStatusUpdateJob.PLATFORMS_COMMA_LIST, platforms);
AgentInventoryStatusUpdateJob.externalizeJobValues(jobDataMap,
AgentInventoryStatusUpdateJob.SERVERS_COMMA_LIST, servers);
trigger.setJobName(DEFAULT_JOB_NAME);
trigger.setJobGroup(DEFAULT_JOB_GROUP);
trigger.setJobDataMap(jobDataMap);
if (isJobScheduled(scheduler, DEFAULT_JOB_NAME, DEFAULT_JOB_GROUP)) {
scheduler.scheduleJob(trigger);
} else {
JobDetail jobDetail = new JobDetail(DEFAULT_JOB_NAME, DEFAULT_JOB_GROUP,
AgentInventoryStatusUpdateJob.class);
scheduler.scheduleJob(jobDetail, trigger);
}
} catch (SchedulerException e) {
LOG.error("Failed to schedule agent inventory update operation.", e);
updateAgentInventoryStatus(platforms, servers);
}
}
/**
* Synchronize the agent's inventory status for platforms, and then the servers,
* omitting servers under synced platforms since they will have been handled
* already. On status change request an agent sync on the affected resources.
* The agent will sync status and determine what other sync work needs to be
* performed.
*
* @param platforms the platforms in inventory
* @param servers the servers in inventory
*/
public void updateAgentInventoryStatus(List<Resource> platforms, List<Resource> servers) {
ResourceSyncInfo syncInfo;
for (Resource platform : platforms) {
AgentClient agentClient = agentManager.getAgentClient(platform.getAgent());
if (agentClient != null) {
try {
//syncInfo = entityManager.find(ResourceSyncInfo.class, platform.getId());
PlatformSyncInfo platformSyncInfo = getPlatformSyncInfo(platform.getAgent());
agentClient.getDiscoveryAgentService().synchronizePlatform(platformSyncInfo);
} catch (Exception e) {
LOG.warn("Could not perform commit synchronization with agent for platform [" + platform.getName()
+ "]", e);
}
} else {
LOG.warn("Could not perform commit sync with agent for platform [" + platform.getName()
+ "]; will expect agent to do it later");
}
}
for (Resource server : servers) {
// Only update servers if they haven't already been updated at the platform level
if (!platforms.contains(server.getParentResource())) {
AgentClient agentClient = agentManager.getAgentClient(server.getAgent());
if (agentClient != null) {
try {
//syncInfo = entityManager.find(ResourceSyncInfo.class, server.getId());
Collection<ResourceSyncInfo> syncInfos = getResourceSyncInfo(server.getId());
agentClient.getDiscoveryAgentService().synchronizeServer(server.getId(), syncInfos);
} catch (Exception e) {
LOG.warn("Could not perform commit synchronization with agent for server [" + server.getName()
+ "]", e);
}
} else {
LOG.warn("Could not perform commit sync with agent for server [" + server.getName()
+ "]; will expect agent to do it later");
}
}
}
}
@Override
public void updateAgentInventoryStatus(String platformsCsvList, String serversCsvList) {
List<Resource> platforms = new ArrayList<Resource>();
AgentInventoryStatusUpdateJob.internalizeJobValues(entityManager, platformsCsvList, platforms);
List<Resource> servers = new ArrayList<Resource>();
AgentInventoryStatusUpdateJob.internalizeJobValues(entityManager, serversCsvList, servers);
updateAgentInventoryStatus(platforms, servers);
}
/**
* Updates statuses according to the inventory rules. This is used internally - never call this yourself without
* knowing what you do. See {@link #updateInventoryStatus(Subject, List, List, InventoryStatus)} for the "public"
* version.
*/
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void updateInventoryStatusInNewTransaction(Subject user, List<Resource> platforms, List<Resource> servers,
InventoryStatus status) {
for (Resource platform : platforms) {
resourceManager.setResourceStatus(user, platform, status, false);
}
for (Resource server : servers) {
resourceManager.setResourceStatus(user, server, status, true);
}
if (status == InventoryStatus.COMMITTED) {
List<Integer> allResourceIds = new ArrayList<Integer>();
for (Resource platform : platforms) {
allResourceIds.add(platform.getId());
}
for (Resource server : servers) {
allResourceIds.add(server.getId());
}
resourceAvailabilityManager.insertNeededAvailabilityForImportedResources(allResourceIds);
}
}
@Override
public Resource manuallyAddResource(Subject subject, int resourceTypeId, int parentResourceId,
Configuration pluginConfiguration) throws Exception {
return manuallyAddResource(subject,
new ImportResourceRequest(resourceTypeId, parentResourceId, pluginConfiguration)).getResource();
}
@Override
public ImportResourceResponse manuallyAddResource(Subject subject, ImportResourceRequest importResourceRequest)
throws InvalidPluginConfigurationClientException, PluginContainerException {
int parentResourceId = importResourceRequest.getParentResourceId();
if (!this.authorizationManager.hasResourcePermission(subject, Permission.CREATE_CHILD_RESOURCES,
parentResourceId)) {
throw new PermissionException("You do not have permission on resource with id " + parentResourceId
+ " to manually add child resources.");
}
ResourceType resourceType = this.resourceTypeManager.getResourceTypeById(subject,
importResourceRequest.getResourceTypeId());
// the subsequent code requires a detached ResourceType param so clear
entityManager.clear();
Resource parentResource = this.resourceManager.getResourceById(subject, parentResourceId);
if (!resourceType.isSupportsManualAdd()) {
throw new RuntimeException("Cannot manually add " + resourceType + " child Resource under parent "
+ parentResource + ", since the " + resourceType + " type does not support manual add.");
}
abortResourceManualAddIfExistingSingleton(parentResource, resourceType);
MergeResourceResponse mergeResourceResponse;
try {
AgentClient agentClient = this.agentManager.getAgentClient(parentResource.getAgent());
DiscoveryAgentService discoveryAgentService = agentClient.getDiscoveryAgentService();
mergeResourceResponse = discoveryAgentService.manuallyAddResource(resourceType, parentResourceId,
importResourceRequest.getPluginConfiguration(), subject.getId());
} catch (CannotConnectException e) {
throw new CannotConnectToAgentException("Error adding [" + resourceType + "] Resource to inventory as "
+ "a child of " + parentResource + " - cause: " + e.getMessage(), e);
} catch (RuntimeException e) {
throw new RuntimeException("Error adding [" + resourceType + "] Resource to inventory as a child of "
+ parentResource + " - cause: " + e, e);
}
Resource resource = resourceManager.getResourceById(subject, mergeResourceResponse.getResourceId());
boolean resourceAlreadyExisted = mergeResourceResponse.resourceAlreadyExisted();
return new ImportResourceResponse(resource, resourceAlreadyExisted);
}
@Override
public MergeResourceResponse addResource(Resource resource, int creatorSubjectId) {
MergeResourceResponse mergeResourceResponse;
try {
validateResource(resource);
} catch (InvalidInventoryReportException e) {
throw new IllegalStateException("Plugin Container sent an invalid Resource - " + e.getLocalizedMessage());
}
if (!initResourceTypes(resource)) {
throw new IllegalStateException("Plugin Container sent a Resource with an unknown type - "
+ resource.getResourceType());
}
Resource existingResource = findExistingResource(resource, null);
if (existingResource != null) {
mergeResourceResponse = new MergeResourceResponse(existingResource.getId(), existingResource.getMtime(),
true);
} else {
Subject creator = this.subjectManager.getSubjectById(creatorSubjectId);
try {
creator = this.subjectManager.loginUnauthenticated(creator.getName());
} catch (LoginException e) {
throw new IllegalStateException(
"Unable to temporarily login to provided resource creator user for resource creation", e);
}
Resource parentResource = this.resourceManager.getResourceById(creator, resource.getParentResource()
.getId());
resource.setAgent(parentResource.getAgent());
resource.setModifiedBy(creator.getName());
// Manually added resources are auto-committed.
resource.setInventoryStatus(InventoryStatus.COMMITTED);
resource.setItime(System.currentTimeMillis());
try {
this.resourceManager.createResource(creator, resource, parentResource.getId());
} catch (ResourceAlreadyExistsException e) {
throw new IllegalStateException(e);
}
mergeResourceResponse = new MergeResourceResponse(resource.getId(), resource.getCtime(), false);
}
return mergeResourceResponse;
}
@Override
public boolean updateResourceVersion(int resourceId, String version) {
Resource existingResource = this.entityManager.find(Resource.class, resourceId);
if (existingResource != null) {
boolean changed = updateResourceVersion(existingResource, version);
if (changed) {
this.entityManager.merge(existingResource);
}
return true;
} else {
return false;
}
}
@Override
@SuppressWarnings("deprecation")
public Set<ResourceUpgradeResponse> upgradeResources(Set<ResourceUpgradeRequest> upgradeRequests) {
Set<ResourceUpgradeResponse> result = new HashSet<ResourceUpgradeResponse>();
boolean allowGenericPropertiesUpgrade = Boolean.parseBoolean(systemManager.getSystemConfiguration(
subjectManager.getOverlord()).getProperty(RHQConstants.AllowResourceGenericPropertiesUpgrade, "false"));
for (ResourceUpgradeRequest request : upgradeRequests) {
Resource existingResource = this.entityManager.find(Resource.class, request.getResourceId());
if (existingResource != null) {
try {
ResourceUpgradeResponse upgradedData = upgradeResource(existingResource, request,
allowGenericPropertiesUpgrade);
if (upgradedData != null) {
result.add(upgradedData);
}
} catch (Exception e) {
LOG.error("Failed to process upgrade request for resource " + existingResource + ".", e);
}
}
}
return result;
}
/**
* Convenience method that looks at <code>resource</code> and if its version is not
* the same as <code>newVersion</code>, its version string will be set to it. If
* the resource's version was different and was changed by this method, <code>true</code>
* will be returned.
*
* @param resource the resource whose version is to be checked
* @param newVersion what the version of the resource should be
*
* @return <code>true</code> if the resource's version was not <code>newVersion</code> and was
* changed to it. <code>false</code> if the version was already the same as <code>newVersion</code>
* or <code>resource</code> was <code>null</code>. In other words, this returns <code>true</code>
* if the resource's version was actually changed.
*/
private boolean updateResourceVersion(Resource resource, String newVersion) {
boolean versionChanged = false;
if (resource != null) {
String oldVersion = resource.getVersion();
// we consider null and "" versions the same - and they should not be product versions
if (oldVersion == null) {
oldVersion = "";
}
if (newVersion == null) {
newVersion = "";
}
versionChanged = !oldVersion.equals(newVersion);
if (versionChanged) {
LOG.info("Resource [" + resource + "] changed its version from [" + oldVersion + "] to [" + newVersion
+ "]");
resource.setVersion(newVersion);
ProductVersion productVersion = null;
if (newVersion.length() > 0) {
productVersion = productVersionManager.addProductVersion(resource.getResourceType(), newVersion);
}
resource.setProductVersion(productVersion);
}
}
return versionChanged;
}
/**
* @param resource NotNull
* @param upgradeRequest
* @param allowGenericPropertiesUpgrade name and description are only upgraded if this is true
* @return response to the upgrade request detailing what has been accepted on the server side
*/
private ResourceUpgradeResponse upgradeResource(Resource resource, ResourceUpgradeRequest upgradeRequest,
boolean allowGenericPropertiesUpgrade) {
if (upgradeRequest.getUpgradeErrorMessage() != null) {
ResourceError error = new ResourceError(resource, ResourceErrorType.UPGRADE,
upgradeRequest.getUpgradeErrorMessage(), upgradeRequest.getUpgradeErrorStackTrace(),
upgradeRequest.getTimestamp());
resourceManager.addResourceError(error);
return null;
}
ResourceUpgradeResponse ret = new ResourceUpgradeResponse();
ret.setResourceId(resource.getId());
if (upgradeRequest.hasSomethingToUpgrade()) {
String resourceKey = upgradeRequest.getNewResourceKey();
String name = upgradeRequest.getNewName();
String description = upgradeRequest.getNewDescription();
String version = upgradeRequest.getNewVersion();
boolean isUpgradeAll = allowGenericPropertiesUpgrade || upgradeRequest.isForceGenericPropertyUpgrade();
StringBuilder logMessage = new StringBuilder("Resource [").append(resource.toString()).append(
"] upgraded [");
if (needsUpgrade(resource.getResourceKey(), resourceKey)) {
resource.setResourceKey(resourceKey);
logMessage.append("resourceKey, ");
ret.setUpgradedResourceKey(resource.getResourceKey());
}
if (isUpgradeAll && needsUpgrade(resource.getName(), name)) {
resource.setName(name);
logMessage.append("name, ");
ret.setUpgradedResourceName(resource.getName());
}
if (isUpgradeAll && needsUpgrade(resource.getDescription(), description)) {
resource.setDescription(description);
logMessage.append("description, ");
ret.setUpgradedResourceDescription(resource.getDescription());
}
if (needsUpgrade(resource.getVersion(), version)) {
resource.setVersion(version);
logMessage.append("version, ");
ret.setUpgradedResourceVersion(resource.getVersion());
}
// If provided, assume the new plugin config should replace the old plugin config in its entirety.
// Use a deep copy without ids as the updgardeRequest config may contain entity config props.
// Note: we explicitly do not call configurationManager.updatePluginConfiguration() because the
// agent is already updated to the new configuration. Instead we call the dedicated local method
// supporting this use case.
Configuration pluginConfig = upgradeRequest.getNewPluginConfiguration();
if (null != pluginConfig) {
pluginConfig = pluginConfig.deepCopy(false);
PluginConfigurationUpdate update = configurationManager.upgradePluginConfiguration(
subjectManager.getOverlord(), resource.getId(), pluginConfig);
ret.setUpgradedResourcePluginConfiguration(update.getResource().getPluginConfiguration());
}
// finally let's remove the potential previous upgrade error. we've now successfully
// upgraded the resource.
List<ResourceError> upgradeErrors = resourceManager.findResourceErrors(subjectManager.getOverlord(),
resource.getId(), ResourceErrorType.UPGRADE);
for (ResourceError error : upgradeErrors) {
entityManager.remove(error);
}
logMessage.replace(logMessage.length() - 1, logMessage.length(), "] to become [")
.append(resource.toString()).append("]");
LOG.info(logMessage.toString());
}
return ret;
}
private void validateInventoryReport(InventoryReport report) throws InvalidInventoryReportException {
for (Resource root : report.getAddedRoots()) {
validateResource(root);
}
}
/**
* @param resource This can be a detached object
* @throws InvalidInventoryReportException
*/
private void validateResource(Resource resource) throws InvalidInventoryReportException {
if (resource.getResourceType() == null) {
throw new InvalidInventoryReportException("Reported resource [" + resource + "] has a null type.");
}
if (resource.getResourceKey() == null) {
throw new InvalidInventoryReportException("Reported resource [" + resource + "] has a null key.");
}
// Recursively validate all the resource's descendants.
for (Resource childResource : resource.getChildResources()) {
validateResource(childResource);
}
}
enum PostMergeAction {
LINK_STORAGE_NODE
}
/**
* <p>Should Not Be Called With Existing Transaction !!!</p>
*
* <p>Merges the specified resource and its children into inventory. If the resource already exists in inventory,
* it is updated; if it does not already exist in inventory, it is added and its parent is set to the specified, already inventoried,
* parent resource.</p>
*
* <p>Does not require an existing transaction. The resource and each child will be merged in an isolated
* transaction</p>
*
* @param resource NotNull pojo, the resource to be merged, should have parent and children pojos set
* @param agent NotNull detached entity, the agent that should be set on the resource being merged
*
* @throws InvalidInventoryReportException if a critical field in the resource is missing or invalid
*/
private void mergeResource(Resource resource, Agent agent) throws InvalidInventoryReportException {
long start = System.currentTimeMillis();
if (LOG.isDebugEnabled()) {
LOG.debug("Merging [" + resource + "]...");
}
// We don't merge the entire resource tree. Instead we batch them in order to reduce transaction overhead
// while ensuring no transaction is too big (and thus risks timeout). To do this we need to flatten the
// tree and chunk through it. Parents must be merged before children, so use a breadth first approach.
// NOTE: this will also strip out all resources that are to be ignored; thus, ignored resources won't get merged
List<Resource> resourceList = treeToBreadthFirstList(resource);
if (LOG.isDebugEnabled()) {
LOG.debug("Preparing to merge [" + resourceList.size() + "] Resources with a batch size of ["
+ MERGE_BATCH_SIZE + "]");
}
Map<Resource, Set<PostMergeAction>> postMergeActions = new HashMap<Resource, Set<PostMergeAction>>();
while (!resourceList.isEmpty()) {
int size = resourceList.size();
int end = (MERGE_BATCH_SIZE < size) ? MERGE_BATCH_SIZE : size;
List<Resource> resourceBatch = resourceList.subList(0, end);
discoveryBoss.mergeResourceInNewTransaction(resourceBatch, agent, postMergeActions);
if (!postMergeActions.isEmpty()) {
performPostMergeActions(postMergeActions);
}
// Advance our progress and possibly help GC. This will remove the processed resources from the backing list
resourceBatch.clear();
}
if (LOG.isDebugEnabled()) {
LOG.debug("Resource and children merged: resource/millis=" + resource.getName() + '/'
+ (System.currentTimeMillis() - start));
}
}
private void performPostMergeActions(Map<Resource, Set<PostMergeAction>> postMergeActions) {
for (Resource r : postMergeActions.keySet()) {
for (PostMergeAction a : postMergeActions.get(r)) {
switch (a) {
case LINK_STORAGE_NODE:
storageNodeManager.linkResource(r);
}
}
}
postMergeActions.clear();
}
private List<Resource> treeToBreadthFirstList(Resource resource) {
// if we are to ignore this resource's type, don't bother doing anything since all is to be ignored
if (resource.getResourceType().isIgnored()) {
return new ArrayList<Resource>(0);
}
List<Resource> result = new ArrayList<Resource>(MERGE_BATCH_SIZE);
LinkedList<Resource> queue = new LinkedList<Resource>();
queue.add(resource);
while (!queue.isEmpty()) {
Resource node = queue.remove();
// if this node is to be ignored, don't traverse it and don't add it to the returned results
if (node.getResourceType().isIgnored()) {
continue;
}
result.add(node);
for (Resource child : node.getChildResources()) {
queue.add(child);
}
}
return result;
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void mergeResourceInNewTransaction(List<Resource> resourceBatch, Agent agent,
Map<Resource, Set<PostMergeAction>> postMergeActions) throws InvalidInventoryReportException {
long batchStart = System.currentTimeMillis();
boolean isDebugEnabled = LOG.isDebugEnabled();
// Cache parent resources we've already fetched from the DB, many resources will have the same parent
Map<Integer, Resource> parentMap = new HashMap<Integer, Resource>();
for (Resource resource : resourceBatch) {
Resource existingResource;
long start = System.currentTimeMillis();
existingResource = findExistingResource(resource, parentMap);
// Does this resource already exist in inventory? If so, update, otherwise add
if (null != existingResource) {
updateExistingResource(resource, existingResource);
} else {
presetAgent(resource, agent);
persistResource(resource, parentMap, postMergeActions);
}
if (isDebugEnabled) {
LOG.debug("Single Resource merged: resource/millis=" + resource.getName() + '/'
+ (System.currentTimeMillis() - start));
}
}
// Help out the GC
parentMap.clear();
if (isDebugEnabled) {
long delta = (System.currentTimeMillis() - batchStart);
LOG.debug("Resource Batch merged: size/average/millis=" + resourceBatch.size() + "/" + delta
/ resourceBatch.size() + "/" + delta);
}
}
/**
* Recursively set the agent on the resource tree.
*
* @param resource pojo, the parent
* @param agent pojo, the agent
*/
private void presetAgent(Resource resource, Agent agent) {
resource.setAgent(agent);
for (Resource child : resource.getChildResources()) {
presetAgent(child, agent);
}
}
/**
* <p>Requires A Transaction</p>
*
* Given a resource, will attempt to find it in the server's inventory (that is, finds it in the database). If the
* given resource's ID does not exist in the database, it will be looked up by its resource key. If the resource
* cannot be found either via ID or resource key then SIDE EFFECT: the given resource's ID will be reset to 0 and null
* will be returned.
*
* @param resource Pojo containing resourceId, key, and parentResoure (if applicable)
* @param parentMap, if supplied, holds previously fetched parent pojos. useful when the calling code does many
* finds for a few parents. If not found in the map the db will be searched, the map will be updated if possible.
* @return the Resource entity found in the database and matching the given resource.
*/
private Resource findExistingResource(Resource resource, Map<Integer, Resource> parentMap) {
boolean isDebugEnabled = LOG.isDebugEnabled();
if (isDebugEnabled) {
LOG.debug("getExistingResource processing for [" + resource + "]");
}
Resource existingResource = null;
if (resource.getId() != 0) {
if (isDebugEnabled) {
LOG.debug("Agent claims resource is already in inventory. Id=" + resource.getId());
}
// This maybe could be more efficient using a named query that pulls some lazy data, but this should be fine
existingResource = entityManager.find(Resource.class, resource.getId());
if (isDebugEnabled) {
if (null != existingResource) {
LOG.debug("Found resource already in inventory. Id=" + resource.getId());
} else {
// agent lied - agent's copy of JON server inventory must be stale.
LOG.debug("However, no resource exists with the specified id. Id=" + resource.getId());
}
}
} else {
LOG.debug("Agent reported resource with id of 0");
}
// If necessary double-check for an existing resource using the business key.
// this will happen if the agent found the resource (non-zero id) but the DB didn't know about it,
// or if the agent didn't know about it to begin with (id was 0).
if (existingResource == null) {
if (isDebugEnabled) {
LOG.debug("Checking if a resource exists with the specified business key. Id=" + resource.getId()
+ ", key=" + resource.getResourceKey());
}
// (jshaughn) I'm not 100% sure but I believe this loop has to do with either or both of:
// - protecting against the agent merging a resource it thinks is new but actually exists
// - handling the case in which a resource type has moved (see f74b22044) and trying to relocate the parent.
// Anyway, I'm not going to touch it even though it slows things down.
ResourceType resourceType = resource.getResourceType();
Resource parent = resource;
while (null != parent && null == existingResource) {
parent = parent.getParentResource();
// check if the parent is in inventory. This might not be the case during initial sync-up for resource upgrade.
Resource existingParent = null;
if (null != parent) {
int parentId = parent.getId();
if (parentId <= 0) {
LOG.warn("Expected potential parent resource to have a valid ID. Parent=" + parent + ", Child="
+ resource);
}
// See if we already fetched this parent and it's in our cache map, if so, use it.
if (null != parentMap) {
existingParent = parentMap.get(parentId);
}
if (null == existingParent) {
existingParent = entityManager.find(Resource.class, parentId);
if (null != existingParent) {
if (null != parentMap) {
parentMap.put(parentId, existingParent);
}
} else {
// this parent is not known to the server, so there's no point in trying to find a child of it...
continue;
}
}
}
// We found the parent in inventory, so now see if we can find this resource in inventory by using
// the parent, the resource key (unique among siblings), the plugin and the type.
Query query = entityManager.createNamedQuery(Resource.QUERY_FIND_BY_PARENT_AND_KEY);
query.setParameter("parent", existingParent);
query.setParameter("key", resource.getResourceKey());
query.setParameter("plugin", resourceType.getPlugin());
query.setParameter("typeName", resourceType.getName());
try {
existingResource = (Resource) query.getSingleResult();
} catch (NoResultException e) {
existingResource = null;
}
}
if (null != existingResource) {
// We found it - reset the id to what it should be.
resource.setId(existingResource.getId());
if (isDebugEnabled) {
LOG.debug("Found resource already in inventory with specified business key, Id=" + resource.getId());
}
} else {
LOG.debug("Unable to find the agent-reported resource by id or business key.");
if (resource.getId() != 0) {
// existingResource is still null at this point, the resource does not exist in inventory.
LOG.error("Resetting the resource's id to zero. Previous Id=" + resource.getId());
resource.setId(0);
// TODO: Is there anything else we should do here to inform the agent it has an out-of-sync resource?
} else {
LOG.debug("Resource's id was already zero, nothing to do for the merge.");
}
}
}
return existingResource;
}
/**
* <p>Requires A Transaction.</p>
* @param updatedResource pojo
* @param existingResource attached entity
* @throws InvalidInventoryReportException
*/
private void updateExistingResource(Resource updatedResource, Resource existingResource)
throws InvalidInventoryReportException {
/*
* there exists a small window of time after the synchronous part of the uninventory and before the async
* quartz job comes along to perform the actual removal of the resource from the database, that an inventory
* report can come across the wire and !OVERWROTE! the UNINVENTORIED status back to COMMITTED. if we find,
* during an inventory report merge, that the existing resource was already uninventoried (indicating that
* the quartz job has not yet come along to remove this resource from the database) we should stop all
* processing from this node and return immediately. this short-cuts the processing for the entire sub-tree
* under this resource, but that's OK because the in-band uninventory logic will have marked entire sub-tree
* for uninventory atomically. in other words, all of the descendants under a resource would also be marked
* for async uninventory too.
*/
if (existingResource.getInventoryStatus() == InventoryStatus.UNINVENTORIED) {
return;
}
Resource existingParent = existingResource.getParentResource();
Resource updatedParent = updatedResource.getParentResource();
ResourceType existingResourceParentType = (existingParent != null) ? existingResource.getParentResource()
.getResourceType() : null;
ResourceType updatedResourceParentType = (updatedParent != null) ? updatedResource.getParentResource()
.getResourceType() : null;
Set<ResourceType> validParentTypes = existingResource.getResourceType().getParentResourceTypes();
if (validParentTypes != null && !validParentTypes.isEmpty()
&& !validParentTypes.contains(existingResourceParentType)) {
// The existing Resource has an invalid parent ResourceType. This may be because its ResourceType was moved
// to a new parent ResourceType, but its new parent was not yet discovered at the time of the type move. See
// if the Resource reported by the Agent has a valid parent type, and, if so, update the existing Resource's
// parent to that type.
if (validParentTypes.contains(updatedResourceParentType)) {
if (existingResource.getParentResource() != null) {
existingResource.getParentResource().removeChildResource(existingResource);
}
if (updatedParent != Resource.ROOT) {
// get attached entity for parent so we can add the child
updatedParent = entityManager.find(Resource.class, updatedParent.getId());
updatedParent.addChildResource(existingResource);
} else {
existingResource.setParentResource(Resource.ROOT);
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Existing Resource " + existingResource + " has invalid parent type ("
+ existingResourceParentType + ") and so does plugin-reported Resource " + updatedResource
+ " (" + updatedResourceParentType + ") - valid parent types are [" + validParentTypes + "].");
}
}
}
// The below block is for Resources that were created via the RHQ GUI, whose descriptions will be null.
if (existingResource.getDescription() == null && updatedResource.getDescription() != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting description of existing resource with id " + existingResource.getId() + " to '"
+ updatedResource.getDescription() + "' (as reported by agent)...");
}
existingResource.setDescription(updatedResource.getDescription());
}
// Log a warning if the agent says the Resource key has changed (should rarely happen).
if ((existingResource.getResourceKey() != null)
&& !existingResource.getResourceKey().equals(updatedResource.getResourceKey())) {
LOG.warn("Agent reported that key for " + existingResource + " has changed from '"
+ existingResource.getResourceKey() + "' to '" + updatedResource.getResourceKey() + "'.");
}
updateResourceVersion(existingResource, updatedResource.getVersion());
}
private boolean initResourceTypes(Resource resource) {
final HashMap<String, ResourceType> types = new HashMap<String, ResourceType>();
try {
return initResourceTypes(resource, types);
} finally {
types.clear(); // help GC
}
}
/**
* recursively assign (detached) ResourceType entities to the resource tree
* @param resource
* @param loadedTypeMap Empty map to start, filled as we go to minimize DB fetches
* @return false if a resource's type is unknown; true if all types were successfully loaded
*/
private boolean initResourceTypes(Resource resource, Map<String, ResourceType> loadedTypeMap) {
String plugin = resource.getResourceType().getPlugin();
String name = resource.getResourceType().getName();
StringBuilder key = new StringBuilder(plugin);
key.append(":::");
key.append(name);
ResourceType resourceType = loadedTypeMap.get(key.toString());
if (null == resourceType) {
try {
resourceType = this.resourceTypeManager.getResourceTypeByNameAndPlugin(name, plugin);
} catch (RuntimeException e) {
resourceType = null;
}
if (null == resourceType) {
return false;
} else {
loadedTypeMap.put(key.toString(), resourceType);
}
}
resource.setResourceType(resourceType);
// don't bother looking at the children if we are just going to ignore this resource
if (resourceType.isIgnored()) {
return true;
}
// bz1174841 protect against the case where resource.hasCustomChildResourcesCollection() is true and the
// custom collection does not support Iterator.remove(). (note, just uses an approach that works in all cases.)
Set<Resource> badChildren = null;
for (Iterator<Resource> childIterator = resource.getChildResources().iterator(); childIterator.hasNext();) {
Resource child = childIterator.next();
if (!initResourceTypes(child, loadedTypeMap)) {
badChildren = (null == badChildren) ? new HashSet<Resource>() : badChildren;
badChildren.add(child);
}
}
if (null != badChildren) {
resource.getChildResources().removeAll(badChildren);
}
return true;
}
private void persistResource(Resource resource, Map<Integer, Resource> parentMap,
Map<Resource, Set<PostMergeAction>> postMergeActions) {
// Id of detached parent resource
Integer parentId = (null != resource.getParentResource()) ? resource.getParentResource().getId() : null;
// attached parentResource
Resource parentResource = null;
if (null != parentId) {
// look in our map cache first
if (null != parentMap) {
parentResource = parentMap.get(parentId);
}
// if not in cache, try the DB
if (null == parentResource) {
// Find the parent resource entity
parentResource = entityManager.find(Resource.class, parentId);
}
// if the parent exists, create the parent-child relationship and add it to the map
if (null != parentResource) {
// We specifically call resource.setParentResource(parentResource) here and not
// parentResource.addChildResource(resource). This is because the former call updates only
// the resource being merged. To use the latter we actually need to pull the child set of the
// parent and add the new child resource to the set. This can be a very intensive thing to do
// for parents with large-cardinality child-sets. This is safe *only* because don't actually
// need the parent's children for any other reason, and we only need to update one side of
// the relation.
resource.setParentResource(parentResource);
parentMap.put(parentId, parentResource);
}
}
ResourceType resourceType = resource.getResourceType();
if (resourceType.isCreatable()) {
// If a newly discovered resource is of a creatable type, search for a matching CreateResourceHistory.
// If it exists, then this resource comes from the resource creation process and we must give it the name
// initially supplied by the user.
CreateResourceHistory matchingHistory = findMatchingCreateResourceHistory(parentId,
resource.getResourceKey());
if (matchingHistory != null) {
String userSuppliedResourceName = matchingHistory.getCreatedResourceName();
if (!isBlank(userSuppliedResourceName) && !userSuppliedResourceName.equals(resource.getName())) {
resource.setName(userSuppliedResourceName);
}
}
}
entityManager.persist(resource);
// Add a product version entry for the new resource.
if ((resource.getVersion() != null) && (resource.getVersion().length() > 0)) {
ProductVersion productVersion = productVersionManager
.addProductVersion(resourceType, resource.getVersion());
resource.setProductVersion(productVersion);
}
// Ensure the new resource has an owner and modifier of superUser.
Subject overlord = subjectManager.getOverlord();
resource.setItime(System.currentTimeMillis());
resource.setModifiedBy(overlord.getName());
setInventoryStatus(parentResource, resource, postMergeActions);
// Extend implicit (recursive) group membership of the parent to the new child
if (null != parentResource) {
groupManager.updateImplicitGroupMembership(overlord, resource);
}
}
private CreateResourceHistory findMatchingCreateResourceHistory(Integer parentId, String resourceKey) {
Query query = PersistenceUtility.createQueryWithOrderBy(entityManager,
CreateResourceHistory.QUERY_FIND_BY_CHILD_RESOURCE_KEY, new PageControl(0, 1, new OrderingField("mtime",
DESC)));
query.setParameter("parentResourceId", parentId);
query.setParameter("newResourceKey", resourceKey);
Iterator iterator = query.getResultList().iterator();
if (iterator.hasNext()) {
CreateResourceHistory next = (CreateResourceHistory) iterator.next();
if (next.getStatus() == SUCCESS) {
return next;
}
}
return null;
}
// Resources are set to either NEW or COMMITTED
// We autocommit in the following scenarios:
// - The parent resource is not a platform and is already committed
// - The resource is a platform and has an RHQ Storage Node child
// - The resource is an RHQ Storage Node child
// Ensure the new resource has the proper inventory status
private void setInventoryStatus(Resource parentResource, Resource resource,
Map<Resource, Set<PostMergeAction>> postMergeActions) {
// never autocommit a platform
if (null == parentResource) {
resource.setInventoryStatus(InventoryStatus.NEW);
return;
}
ResourceType resourceType = resource.getResourceType();
boolean isParentCommitted = InventoryStatus.COMMITTED == parentResource.getInventoryStatus();
boolean isService = ResourceCategory.SERVICE == resourceType.getCategory();
boolean isParentServer = ResourceCategory.SERVER == parentResource.getResourceType().getCategory();
// always autocommit non-top-level-server children of committed parents
if (isParentCommitted && (isService || isParentServer)) {
resource.setInventoryStatus(InventoryStatus.COMMITTED);
return;
}
// always autocommit top-level-server if it's an RHQ Storage Node (and the platform, if necessary)
boolean isStorageNodePlugin = "RHQStorage".equals(resourceType.getPlugin());
boolean isStorageNode = (isStorageNodePlugin && "RHQ Storage Node".equals(resourceType.getName()));
if (isStorageNode) {
resource.setInventoryStatus(InventoryStatus.COMMITTED);
if (!isParentCommitted) {
parentResource.setInventoryStatus(InventoryStatus.COMMITTED);
}
addPostMergeAction(postMergeActions, resource, PostMergeAction.LINK_STORAGE_NODE);
return;
}
// otherwise, set NEW
resource.setInventoryStatus(InventoryStatus.NEW);
}
private void addPostMergeAction(Map<Resource, Set<PostMergeAction>> postMergeActions, Resource resource,
PostMergeAction action) {
if (postMergeActions.containsKey(resource)) {
postMergeActions.get(resource).add(action);
} else {
postMergeActions.put(resource, EnumSet.of(PostMergeAction.LINK_STORAGE_NODE));
}
}
@Override
public void importResources(Subject subject, int[] resourceIds) {
if (resourceIds == null || resourceIds.length == 0) {
return;
}
checkStatus(subject, resourceIds, InventoryStatus.COMMITTED, EnumSet.of(InventoryStatus.NEW));
}
@Override
public void ignoreResources(Subject subject, int[] resourceIds) {
if (resourceIds == null || resourceIds.length == 0) {
return;
}
checkStatus(subject, resourceIds, InventoryStatus.IGNORED,
EnumSet.of(InventoryStatus.NEW, InventoryStatus.COMMITTED));
// Previously we set all availabilities for the ignored resources UNKNOWN because they won't be tracked
// after this point. The problem (see Bug 1053888) is that the underlying code did not use locking and
// therefore could conflict with agent availReporting/Shutdown/Backfill. To do it right would mean that
// we'd need to process the resources for each agent separately, so that a proper AvailabilitySerializationLock
// could be applied. Instead of incurring the overhead of creating the necessary Map for Agents to
// Resources, we're just going to leave the Availabilities as is. It's unlikely to cause confusion but if it
// does then this decision/code can ve revisited. For now, commenting out the old, flawed code-block:
/*
// We want to set all availabilities for the ignored resources to "unknown" since we won't be tracking them anymore.
// This is more of a convienence; if it fails for any reason, just log an error but don't roll back ignoring the resources.
try {
this.availabilityManager.setResourceAvailabilities(resourceIds, AvailabilityType.UNKNOWN);
} catch (Exception e) {
LOG.error("Failed to reset availabilities for resources being ignored: " + ThrowableUtil.getAllMessages(e));
}
*/
}
@Override
public void unignoreResources(Subject subject, int[] resourceIds) {
if (resourceIds == null || resourceIds.length == 0) {
return;
}
checkStatus(subject, resourceIds, InventoryStatus.NEW, EnumSet.of(InventoryStatus.IGNORED));
}
@Override
public void unignoreAndImportResources(Subject subject, int[] resourceIds) {
if (resourceIds == null || resourceIds.length == 0) {
return;
}
checkStatus(subject, resourceIds, InventoryStatus.COMMITTED, EnumSet.of(InventoryStatus.IGNORED));
}
@SuppressWarnings("unchecked")
private void checkStatus(Subject subject, int[] resourceIds, InventoryStatus target,
EnumSet<InventoryStatus> validStatuses) {
Query query = entityManager.createQuery("" //
+ " SELECT res.inventoryStatus " //
+ " FROM Resource res " //
+ " WHERE res.id IN ( :resourceIds ) " //
+ "GROUP BY res.inventoryStatus ");
List<Integer> resourceIdList = ArrayUtils.wrapInList(resourceIds);
// Do one query per 1000 Resource id's to prevent Oracle from failing because of an IN clause with more
// than 1000 items.
// After the below while loop completes, this Set will contain the statuses represented by the Resources with
// the passed in id's.
Set<InventoryStatus> statuses = EnumSet.noneOf(InventoryStatus.class);
int fromIndex = 0;
while (fromIndex < resourceIds.length) {
int toIndex = (resourceIds.length < (fromIndex + 1000)) ? resourceIds.length : (fromIndex + 1000);
List<Integer> resourceIdSubList = resourceIdList.subList(fromIndex, toIndex);
query.setParameter("resourceIds", resourceIdSubList);
List<InventoryStatus> batchStatuses = query.getResultList();
statuses.addAll(batchStatuses);
fromIndex = toIndex;
}
if (!validStatuses.containsAll(statuses)) {
throw new IllegalArgumentException("Can only set inventory status to [" + target
+ "] for Resources with current inventory status of one of [" + validStatuses + "].");
}
// Do one query per 1000 Resource id's to prevent Oracle from failing because of an IN clause with more
// than 1000 items.
List<Resource> resources = new ArrayList<Resource>(resourceIds.length);
fromIndex = 0;
while (fromIndex < resourceIds.length) {
int toIndex = (resourceIds.length < (fromIndex + 1000)) ? resourceIds.length : (fromIndex + 1000);
int[] resourceIdSubArray = Arrays.copyOfRange(resourceIds, fromIndex, toIndex);
PageList<Resource> batchResources = resourceManager.findResourceByIds(subject, resourceIdSubArray, false,
PageControl.getUnlimitedInstance());
resources.addAll(batchResources);
fromIndex = toIndex;
}
// Split the Resources into two lists - one for platforms and one for servers, since that's what
// updateInventoryStatus() expects.
List<Resource> platforms = new ArrayList<Resource>();
List<Resource> servers = new ArrayList<Resource>();
for (Resource resource : resources) {
ResourceCategory category = resource.getResourceType().getCategory();
if (category == ResourceCategory.PLATFORM) {
if (target == InventoryStatus.IGNORED && (resource.getInventoryStatus() == InventoryStatus.COMMITTED)) {
LOG.warn("Cannot ignore a committed platform - skipping request to ignore:" + resource);
} else {
platforms.add(resource);
}
} else {
servers.add(resource); // we include services in here now (see BZ 535289)
}
}
updateInventoryStatus(subject, platforms, servers, target);
}
private <T> boolean needsUpgrade(T oldValue, T newValue) {
return newValue != null && (oldValue == null || !newValue.equals(oldValue));
}
private void abortResourceManualAddIfExistingSingleton(Resource parentResource, ResourceType resourceType) {
if (resourceType.isSingleton()) {
ResourceCriteria resourceCriteria = new ResourceCriteria();
resourceCriteria.addFilterParentResourceId(parentResource.getId());
resourceCriteria.addFilterResourceTypeId(resourceType.getId());
resourceCriteria.clearPaging();//Doc: disable paging as the code assumes all the results will be returned.
PageList<Resource> childResourcesOfType = resourceManager.findResourcesByCriteria(
subjectManager.getOverlord(), resourceCriteria);
if (childResourcesOfType.size() >= 1) {
throw new RuntimeException("Cannot manually add " + resourceType + " child Resource under parent "
+ parentResource + ", since " + resourceType
+ " is a singleton type, and there is already a child Resource of that type. "
+ "If the existing child Resource corresponds to a managed Resource which no longer exists, "
+ "uninventory it and then try again.");
}
}
}
}