/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.resource.cluster;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.resource.group.ClusterKey;
import org.rhq.core.domain.resource.group.GroupCategory;
import org.rhq.core.domain.resource.group.ResourceGroup;
import org.rhq.core.domain.resource.group.composite.ClusterFlyweight;
import org.rhq.core.domain.resource.group.composite.ClusterKeyFlyweight;
import org.rhq.enterprise.server.RHQConstants;
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.resource.group.ResourceGroupAlreadyExistsException;
import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal;
/**
*
* @author jay shaughnessy
*
*/
@Stateless
public class ClusterManagerBean implements ClusterManagerLocal, ClusterManagerRemote {
private final Log log = LogFactory.getLog(ClusterManagerBean.class);
@PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
private EntityManager entityManager;
@EJB
private ResourceGroupManagerLocal resourceGroupManager;
@EJB
private AuthorizationManagerLocal authorizationManager;
@EJB
private SubjectManagerLocal subjectManager;
public ResourceGroup createAutoClusterBackingGroup(Subject subject, ClusterKey clusterKey, boolean addResources) {
ResourceGroup autoClusterBackingGroup = null;
Query query = entityManager.createNamedQuery(ResourceGroup.QUERY_FIND_BY_CLUSTER_KEY);
query.setParameter("clusterKey", clusterKey.toString());
ResourceType resourceType = entityManager.find(ResourceType.class, ClusterKey.getResourceType(clusterKey));
ResourceGroup resourceGroup = entityManager.find(ResourceGroup.class, clusterKey.getClusterGroupId());
if (!authorizationManager.canViewGroup(subject, clusterKey.getClusterGroupId())) {
throw new PermissionException("You do not have permission to view child cluster groups of the group ["
+ resourceGroup.getName() + "]");
}
// [BZ 817604] In unusual circumstances this clusterKey may be stale. Ensure the cluster group
// is still a compat group before creating a backing group.
if (GroupCategory.COMPATIBLE != resourceGroup.getGroupCategory()) {
throw new IllegalStateException("The root group has changed. Please refresh your group before continuing.");
}
List<Resource> resources = null;
try {
autoClusterBackingGroup = (ResourceGroup) query.getSingleResult();
} catch (NoResultException nre) {
try {
resources = getAutoClusterResources(subject, clusterKey);
String name = null;
if (resources.isEmpty()) {
name = "Group of " + resourceType.getName();
} else {
for (Resource res : resources) {
if (name == null) {
name = res.getName();
} else {
if (!name.equals(res.getName())) {
name = "Group of " + resourceType.getName();
}
}
}
}
// Note, group names do not need to be unique for non-visible groups, like autocluster backing groups
autoClusterBackingGroup = new ResourceGroup(name, resourceType);
autoClusterBackingGroup.setClusterKey(clusterKey.toString());
autoClusterBackingGroup.setClusterResourceGroup(resourceGroup);
autoClusterBackingGroup.setVisible(false);
// You are allowed to cause the creation of an auto cluster backing group as long as you can
// view the parent group. (That check was done above)
int id = resourceGroupManager
.createResourceGroup(subjectManager.getOverlord(), autoClusterBackingGroup).getId();
autoClusterBackingGroup = entityManager.find(ResourceGroup.class, id);
} catch (ResourceGroupAlreadyExistsException e) {
// This should not happen since the group name is actually generated
log.error("Unexpected Error, group exists " + e);
return null;
}
}
if (addResources) {
if (resources == null) {
resources = getAutoClusterResources(subject, clusterKey);
}
int i = 0;
int[] resourceIds = new int[resources.size()];
for (Resource res : resources) {
resourceIds[i++] = res.getId();
}
try {
// You are allowed to cause the creation of an auto cluster backing group as long as you can
// view the parent group. (That check was done above)
resourceGroupManager.setAssignedResources(subjectManager.getOverlord(),
autoClusterBackingGroup.getId(), resourceIds, false);
} catch (Exception e) {
log.error("Could not add resources to group:" + e);
}
}
return autoClusterBackingGroup;
}
public ResourceGroup getAutoClusterBackingGroup(Subject subject, ClusterKey clusterKey) {
ResourceGroup result = null;
Query query = entityManager.createNamedQuery(ResourceGroup.QUERY_FIND_BY_CLUSTER_KEY);
query.setParameter("clusterKey", clusterKey.toString());
try {
result = (ResourceGroup) query.getSingleResult();
} catch (NoResultException e) {
result = null;
}
return result;
}
@SuppressWarnings("unchecked")
public List<Resource> getAutoClusterResources(Subject subject, ClusterKey clusterKey) {
// Build the query
Map<String, Object> params = new HashMap<String, Object>();
String queryString = getClusterKeyQuery(clusterKey, params);
if (log.isDebugEnabled()) {
log.debug("getAutoClusterResources() generated query: " + queryString);
}
Query query = entityManager.createQuery(queryString);
for (Map.Entry<String, Object> param : params.entrySet()) {
query.setParameter(param.getKey(), param.getValue());
}
List<Resource> rs = query.getResultList();
return rs;
}
public ClusterFlyweight getClusterTree(Subject subject, int groupId) {
final String queryString = //
"SELECT r.id, r.resourceType.id, r.parentResource.id, r.resourceKey, r.name, " //
+ " (SELECT count(r2)" //
+ " FROM Resource r2 JOIN r2.explicitGroups g2 " //
+ " WHERE g2.id = :groupId and r2.id = r.id and r2.inventoryStatus = 'COMMITTED') " //
+ " FROM Resource r join r.implicitGroups g " //
+ " WHERE g.id = :groupId and r.inventoryStatus = 'COMMITTED' ";
Query query = entityManager.createQuery(queryString);
query.setParameter("groupId", groupId);
List<Object[]> rs = query.getResultList();
Map<Integer, List<ClusterTreeQueryResults>> dataMap = new HashMap<Integer, List<ClusterTreeQueryResults>>();
Set<Integer> explicitResources = new HashSet<Integer>();
for (Object[] d : rs) {
ClusterTreeQueryResults row = new ClusterTreeQueryResults(d);
Integer parentId = row.parentId;
List<ClusterTreeQueryResults> childList = dataMap.get(parentId);
if (childList == null) {
childList = new ArrayList<ClusterTreeQueryResults>();
dataMap.put(parentId, childList);
}
childList.add(row);
if (row.isExplicitResource) {
explicitResources.add(row.resourceId);
}
}
ClusterFlyweight topTreeNode = new ClusterFlyweight(groupId);
// dataMap contains one key for every *parent resource* that is a parent to a group resource.
// this could include platform resources if group members are top level servers.
buildTree(groupId, topTreeNode, explicitResources, dataMap);
return topTreeNode;
}
/**
* Recursively builds tree nodes (where tree nodes are of type ClusterFlyweight).
* The <code>parent</code> object will be modified - it is the tree that is being built.
* The parent node reprents a single cluster node, where a cluster node is an aggregation
* of N identical resources that belong to the group (where "identical" means the same
* resource type and resource key).
*
* @param groupId identifies the group whose tree is being built
* @param parent the top "parent cluster node" of the tree - will get its children assigned to it via this method
* @param parentIds the resource IDs for all identical parent resources that make up the one "parent cluster node"
* @param data keyed on resource ID whose list is that of the resource's children. A few of these data
* won't necessarily be placed in the "parent" top node because these may not be part of the tree, but
* may be parents themselves of the top most resources (for example, if I have a group of JBossAS
* Server resources, some of these data will be the platform resources since they are parents to the
* JBossAS Server resources - those platforms will not be represented in the "parent" top tree node).
* Every parent resource, regardless of where they are in the cluster hierarchy, is represented by a key
* in this data map.
*/
private void buildTree(int groupId, ClusterFlyweight parent, Set<Integer> parentIds,
Map<Integer, List<ClusterTreeQueryResults>> data) {
// this is the children cluster nodes for the parent cluster node we are building
// notice the key to this map is a ClusterKeyFlyweight, which is a resourceType/resourceKey tuple
Map<ClusterKeyFlyweight, ClusterFlyweight> children = new HashMap<ClusterKeyFlyweight, ClusterFlyweight>();
// has the same as children above except its values aren't child nodes, but just the child nodes' resource IDs
Map<ClusterKeyFlyweight, Set<Integer>> members = new HashMap<ClusterKeyFlyweight, Set<Integer>>();
// we would expect a maximum number of identical child resources to be the same as the number of parents we have.
// in other words, this happens when each individual parent resource has an identical child (which is the typical use case).
int maxChildrenExpected = parentIds.size();
// loop through each identical parent resource to aggregate their children into a single cluster node
for (Integer parentId : parentIds) {
List<ClusterTreeQueryResults> directChildren = data.get(parentId);
if (directChildren != null) {
for (ClusterTreeQueryResults child : directChildren) {
ClusterKeyFlyweight childNodeKey = new ClusterKeyFlyweight(child.resourceTypeId, child.resourceKey);
ClusterFlyweight childNode = children.get(childNodeKey);
Set<Integer> memberList;
if (childNode == null) {
childNode = new ClusterFlyweight(childNodeKey);
childNode.setClusterSize(maxChildrenExpected);
children.put(childNodeKey, childNode);
memberList = new HashSet<Integer>();
members.put(childNodeKey, memberList);
} else {
memberList = members.get(childNodeKey);
}
childNode.addResource(child.resourceName);
childNode.incrementMembers();
memberList.add(child.resourceId);
}
}
}
// now that we have aggregated all the children for the individual yet identical parent resources, assign those
// aggregated child nodes to the parent cluster node
parent.setChildren(new ArrayList<ClusterFlyweight>(children.values()));
for (ClusterFlyweight child : children.values()) {
buildTree(groupId, child, members.get(child.getClusterKey()), data);
}
return;
}
private class ClusterTreeQueryResults {
Integer resourceId;
Integer resourceTypeId;
Integer parentId;
String resourceKey;
String resourceName;
boolean isExplicitResource;
private ClusterTreeQueryResults(Object[] queryResults) {
resourceId = (Integer) queryResults[0];
resourceTypeId = (Integer) queryResults[1];
parentId = (Integer) queryResults[2];
resourceKey = (String) queryResults[3];
resourceName = (String) queryResults[4];
isExplicitResource = ((Number) queryResults[5]).intValue() > 0;
}
}
private String getClusterKeyQuery(ClusterKey clusterKey, Map<String, Object> params) {
if (null == clusterKey)
return null;
if (0 == clusterKey.getDepth())
return null;
StringBuilder query = new StringBuilder();
buildQuery(query, params, clusterKey, clusterKey.getHierarchy());
return query.toString();
}
/**
* Builds a query like the following (this is a depth-2 example):
* <pre>
* SELECT r2 FROM Resource r2
* WHERE r2.resourceKey = :r2key AND r2.resourceType = :r2rt AND r2.parentResource IN (
* SELECT r1 FROM Resource r1
* WHERE r1.resourceKey = :r1key AND r1.resourceType = :r1rt AND r1.parentResource IN (
* SELECT rgir FROM ResourceGroup rg JOIN rg.implicitResources rgir
* WHERE rg = :rgId
* </pre>
*
* Some of the parameters are actually filled in with the literal values, however, the params
* map will be filled with some parameters that need to be set on the query before the query can
* be executed. This is necessary to avoid having to escape characters (like backslashes) that might
* exist in resource keys.
*/
private void buildQuery(StringBuilder query, Map<String, Object> params, ClusterKey clusterKey,
List<ClusterKey.Node> nodes) {
int size = nodes.size();
ClusterKey.Node node = nodes.get(size - 1);
String alias = "r" + size;
String keyParam = "key" + size;
// TODO: Change subquery syntax to be like the JOIN below.
query.append(" SELECT " + alias + " FROM Resource " + alias + " WHERE ");
query.append(alias + ".resourceKey = :" + keyParam + " AND ");
query.append(alias + ".resourceType = " + node.getResourceTypeId() + " AND ");
query.append(alias + ".parentResource IN ( ");
params.put(keyParam, node.getResourceKey());
// this is an authorization-related query, so use implicitResource (not explicitResources)
if (1 == size) {
query
.append("SELECT rgir FROM ResourceGroup rg JOIN rg.implicitResources rgir WHERE rgir.inventoryStatus = 'COMMITTED' AND rg = "
+ clusterKey.getClusterGroupId());
} else {
buildQuery(query, params, clusterKey, nodes.subList(0, size - 1));
}
query.append(")");
}
}