/*
* RHQ Management Platform
* Copyright (C) 2005-2013 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License 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.rest;
import java.net.URI;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiError;
import com.wordnik.swagger.annotations.ApiErrors;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import org.jboss.resteasy.annotations.GZIP;
import org.rhq.core.domain.alert.AlertCondition;
import org.rhq.core.domain.alert.AlertConditionCategory;
import org.rhq.core.domain.alert.AlertDampening;
import org.rhq.core.domain.alert.AlertDefinition;
import org.rhq.core.domain.alert.AlertPriority;
import org.rhq.core.domain.alert.BooleanExpression;
import org.rhq.core.domain.alert.notification.AlertNotification;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.domain.criteria.AlertDefinitionCriteria;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementDefinition;
import org.rhq.core.domain.operation.OperationDefinition;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.resource.group.GroupCategory;
import org.rhq.core.domain.resource.group.ResourceGroup;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.domain.util.PageOrdering;
import org.rhq.core.util.StringUtil;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.alert.AlertConditionManagerLocal;
import org.rhq.enterprise.server.alert.AlertDefinitionManagerLocal;
import org.rhq.enterprise.server.alert.AlertManagerLocal;
import org.rhq.enterprise.server.alert.AlertNotificationManagerLocal;
import org.rhq.enterprise.server.operation.OperationManagerLocal;
import org.rhq.enterprise.server.plugin.pc.alert.AlertSenderInfo;
import org.rhq.enterprise.server.plugin.pc.alert.AlertSenderPluginManager;
import org.rhq.enterprise.server.resource.ResourceTypeManagerLocal;
import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal;
import org.rhq.enterprise.server.rest.domain.AlertConditionRest;
import org.rhq.enterprise.server.rest.domain.AlertDefinitionRest;
import org.rhq.enterprise.server.rest.domain.AlertNotificationRest;
import org.rhq.enterprise.server.rest.domain.AlertSender;
import org.rhq.enterprise.server.rest.domain.Link;
import org.rhq.enterprise.server.rest.helper.ConfigurationHelper;
/**
* Deal with Alert Definitions. Note that this class shares the /alert/ sub-context with the
* AlertHandlerBean
* @author Heiko W. Rupp
*/
@Path("/alert")
@Api(value = "Deal with Alert Definitions",description = "This api deals with alert definitions.")
@Stateless
@Interceptors(SetCallerInterceptor.class)
public class AlertDefinitionHandlerBean extends AbstractRestBean {
@EJB
private AlertDefinitionManagerLocal alertDefinitionManager;
@EJB
private AlertNotificationManagerLocal notificationMgr;
@EJB
private AlertConditionManagerLocal conditionMgr;
@EJB
private AlertManagerLocal alertManager;
@EJB
private ResourceGroupManagerLocal resourceGroupMgr;
@EJB
private ResourceTypeManagerLocal resourceTypeMgr;
@EJB
private OperationManagerLocal operationMgr;
@PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
private EntityManager entityManager;
// Redirect from /definition to /definitions for GET requests
@GET
@Path("/definition")
@ApiOperation(value = "Redirects to /alert/definitions")
public Response redirectDefinitionToDefinitions(@Context UriInfo uriInfo) {
UriBuilder uriBuilder = uriInfo.getRequestUriBuilder();
String path = uriInfo.getPath();
path = path.replace("/definition","/definitions");
uriBuilder.replacePath("/rest" + path);
Response.ResponseBuilder builder = Response.seeOther(uriBuilder.build());
return builder.build();
}
@GZIP
@GET
@Path("/definitions")
@ApiOperation(value = "List all Alert Definition", responseClass = "AlertDefinitionRest", multiValueResponse = true)
public Response listAlertDefinitions(
@ApiParam("Should conditions and notifications be returned too?") @QueryParam("full") @DefaultValue("false") boolean full,
@ApiParam(value = "Page number") @QueryParam("page") Integer page,
@ApiParam(value = "Page size") @DefaultValue("20") @QueryParam("ps") int pageSize,
@ApiParam(value = "Resource id to filter by") @QueryParam("resourceId") Integer resourceId,
@Context HttpHeaders headers,
@Context UriInfo uriInfo) {
AlertDefinitionCriteria criteria = new AlertDefinitionCriteria();
criteria.addSortId(PageOrdering.ASC);
if (page!=null) {
criteria.setPaging(page,pageSize);
}
if (resourceId!=null) {
criteria.addFilterResourceIds(resourceId);
}
PageList<AlertDefinition> defs = alertDefinitionManager.findAlertDefinitionsByCriteria(caller, criteria);
List<AlertDefinitionRest> ret = new ArrayList<AlertDefinitionRest>(defs.size());
for (AlertDefinition def : defs) {
AlertDefinitionRest adr = definitionToDomain(def, full, uriInfo);
ret.add(adr);
}
Response.ResponseBuilder builder = Response.ok();
MediaType mediaType = headers.getAcceptableMediaTypes().get(0);
builder.type(mediaType);
if (mediaType.equals(wrappedCollectionJsonType)) {
wrapForPaging(builder,uriInfo,defs,ret);
} else {
createPagingHeader(builder,uriInfo,defs);
if (mediaType.equals(MediaType.APPLICATION_XML_TYPE)) {
GenericEntity<List<AlertDefinitionRest>> list = new GenericEntity<List<AlertDefinitionRest>>(ret) {
};
builder.entity(list);
} else {
builder.entity(ret);
}
}
return builder.build();
}
@GET
@Path("/definition/{id}")
@ApiOperation(value = "Get one AlertDefinition by id", responseClass = "AlertDefinitionRest")
@ApiError(code = 404, reason = "No definition found with the passed id.")
public Response getAlertDefinition(@ApiParam("Id of the alert definition to retrieve") @PathParam("id") int definitionId,
@ApiParam("Should conditions and notifications be returned too?") @QueryParam("full") @DefaultValue("true") boolean full,
@ApiParam("Should deleted definition be retrieved?") @QueryParam("deleted") @DefaultValue("true") boolean deleted,
@Context Request request, @Context UriInfo uriInfo) {
AlertDefinition def = alertDefinitionManager.getAlertDefinition(caller, definitionId);
if (def == null || (def.getDeleted() && !deleted)) {
throw new StuffNotFoundException("AlertDefinition with id " + definitionId );
}
EntityTag eTag = new EntityTag(Integer.toHexString(def.hashCode()));
Response.ResponseBuilder builder = request.evaluatePreconditions(eTag);
if (builder==null) {
AlertDefinitionRest adr = definitionToDomain(def, full, uriInfo);
builder = Response.ok(adr);
}
builder.tag(eTag);
return builder.build();
}
@POST
@Path("/definitions")
@ApiOperation(value="Create an AlertDefinition for the resource/group/resource type passed as query param. " +
"One and only one of the three params must be given at any time. Please also check the POST method " +
"for conditions and notifications to see their options")
@ApiErrors({
@ApiError(code = 406, reason = "There was not exactly one of 'resourceId','groupId' or 'resourceTypeId' given"),
@ApiError(code = 406, reason = "The passed condition failed validation"),
@ApiError(code = 406, reason = "The passed group was a mixed group, that can not have alert definitions"),
@ApiError(code = 404, reason = "A non existing alert notification sender was requested."),
@ApiError(code = 404, reason = "A referenced alert to recover does not exist")
})
public Response createAlertDefinition(@ApiParam("The id of the resource to attach the definition to") @QueryParam("resourceId") Integer resourceId,
@ApiParam("The id of the group to attach the definition to") @QueryParam("groupId") Integer groupId,
@ApiParam("The id of the resource type to attach the definition to") @QueryParam("resourceTypeId") Integer resourceTypeId,
@ApiParam("The data for the new definition") AlertDefinitionRest adr,
@Context UriInfo uriInfo) {
int i = 0;
if (resourceId!=null) i++;
if (groupId!=null) i++;
if (resourceTypeId!=null) i++;
if (i!=1) {
throw new BadArgumentException("query param","You must give exactly one query param out of 'resourceId', 'groupId' or 'resourceTypeId'");
}
AlertDefinition alertDefinition = new AlertDefinition();
alertDefinition.setName(adr.getName());
alertDefinition.setEnabled(adr.isEnabled());
if (adr.getPriority()==null) {
adr.setPriority("LOW");
}
alertDefinition.setPriority(AlertPriority.valueOf(adr.getPriority().toUpperCase()));
alertDefinition.setConditionExpression(BooleanExpression.valueOf(adr.getConditionMode().toUpperCase()));
alertDefinition.setRecoveryId(adr.getRecoveryId());
Resource resource = null;
if (resourceId!= null) {
resource = fetchResource(resourceId);
}
ResourceType resourceType=null;
if (groupId!=null) {
ResourceGroup group = resourceGroupMgr.getResourceGroup(caller,groupId);
alertDefinition.setGroup(group);
if (group.getGroupCategory()== GroupCategory.MIXED) {
throw new BadArgumentException("Group with id " + +groupId + " is a mixed group");
}
resourceType = group.getResourceType(); // TODO this may be null -> check 1st resource
}
if (resourceTypeId!=null) {
resourceType = resourceTypeMgr.getResourceTypeById(caller,resourceTypeId);
alertDefinition.setResourceType(resourceType);
}
Set<AlertCondition> conditions = new HashSet<AlertCondition>(adr.getConditions().size());
for (AlertConditionRest acr : adr.getConditions()) {
AlertCondition condition = conditionRestToCondition(acr, resource, resourceType);
conditions.add(condition);
}
alertDefinition.setConditions(conditions);
List<AlertNotification> notifications = new ArrayList<AlertNotification>(adr.getNotifications().size());
// check if the sender by name exists
AlertSenderPluginManager pluginManager = alertManager.getAlertPluginManager();
for (AlertNotificationRest anr : adr.getNotifications()) {
AlertNotification notification = notificationRestToNotification(alertDefinition, anr);
notifications.add(notification);
}
alertDefinition.setAlertNotifications(notifications);
setDampeningFromRest(alertDefinition, adr);
// Set the recovery id if such a definition exists at all
if (adr.getRecoveryId()>0) {
AlertDefinition recoveryDef = alertDefinitionManager.getAlertDefinition(caller,adr.getRecoveryId());
if (recoveryDef!=null)
alertDefinition.setRecoveryId(adr.getRecoveryId());
else
throw new StuffNotFoundException("Recovery alert with id " + adr.getRecoveryId());
}
AlertDefinition updatedDefinition = alertDefinitionManager.createAlertDefinitionInNewTransaction(caller,
alertDefinition, resourceId, false);
int definitionId = updatedDefinition.getId();
AlertDefinitionRest uadr = definitionToDomain(updatedDefinition,true, uriInfo) ; // TODO param 'full' ?
uadr.setId(definitionId);
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/alert/definition/{id}");
URI uri = uriBuilder.build(definitionId);
Response.ResponseBuilder builder = Response.created(uri);
builder.entity(uadr);
return builder.build();
}
@PUT
@Path("/definition/{id}")
@ApiOperation(value = "Update the alert definition (priority, enablement, dampening, recovery)", notes = "Priority must be HIGH,LOW,MEDIUM. If not provided, LOW is assumed.")
@ApiError(code = 404, reason = "No AlertDefinition with the passed id exists")
public Response updateDefinition(
@ApiParam("Id of the alert definition to update") @PathParam("id") int definitionId,
@ApiParam("Data for the update") AlertDefinitionRest definitionRest,
@Context UriInfo uriInfo) {
AlertDefinition definition = alertDefinitionManager.getAlertDefinition(caller,definitionId);
if (definition==null) {
throw new StuffNotFoundException("AlertDefinition with id " + definitionId);
}
definition = new AlertDefinition(definition); // detach
definition.setEnabled(definitionRest.isEnabled());
if (definitionRest.getPriority()!=null) {
definition.setPriority(AlertPriority.valueOf(definitionRest.getPriority()));
}
else {
definition.setPriority(AlertPriority.LOW);
}
setDampeningFromRest(definition, definitionRest);
// Set the recovery id if such a definition exists at all
if (definitionRest.getRecoveryId()>0) {
AlertDefinition recoveryDef = alertDefinitionManager.getAlertDefinition(caller,definitionRest.getRecoveryId());
if (recoveryDef!=null) {
definition.setRecoveryId(definitionRest.getRecoveryId());
}
else {
throw new StuffNotFoundException("Alert to recover with id " + definitionRest.getRecoveryId());
}
}
definition = alertDefinitionManager.updateAlertDefinitionInternal(caller, definitionId, definition, true,
true, true);
entityManager.flush();
EntityTag eTag = new EntityTag(Integer.toHexString(definition.hashCode()));
AlertDefinitionRest adr = definitionToDomain(definition, false, uriInfo);
Response.ResponseBuilder builder = Response.ok(adr);
builder.tag(eTag);
return builder.build();
}
@DELETE
@Path("definition/{id}")
@ApiOperation(value = "Delete an alert definition", notes = "This operation is by default idempotent, returning 204." +
"If you want to check if the definition existed at all, you need to pass the 'validate' query parameter.")
@ApiErrors({
@ApiError(code = 204, reason = "Definition was deleted or did not exist with validation not set"),
@ApiError(code = 404, reason = "Definition did not exist and validate was set")
})
public Response deleteDefinition(@ApiParam("Id of the definition to delete") @PathParam("id") int definitionId,
@ApiParam("Validate if the definition exists") @QueryParam("validate") @DefaultValue("false") boolean validate) {
int count = alertDefinitionManager.removeAlertDefinitions(caller, new int[]{definitionId});
if (count == 0 && validate) {
throw new StuffNotFoundException("Definition with id " + definitionId);
}
return Response.noContent().build();
}
/**
* Create a dampening object for the passed definition from the alert definition rest that is passed in.
* @param alertDefinition The alert definition to modify
* @param adr The incoming AlertDefinitonRest object
*/
private void setDampeningFromRest(AlertDefinition alertDefinition, AlertDefinitionRest adr) {
AlertDampening.Category dampeningCategory;
try {
dampeningCategory = AlertDampening.Category.valueOf(adr.getDampeningCategory().toUpperCase());
}
catch (Exception e) {
AlertDampening.Category[] vals = AlertDampening.Category.values();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < vals.length ; i++) {
builder.append(vals[i].name());
if (i < vals.length-1) {
builder.append(", ");
}
}
throw new BadArgumentException("dampening category","Allowed values are: " + builder.toString());
}
if (dampeningCategory == AlertDampening.Category.ONCE) {
// WillRecover = true means to disable after firing
// See org.rhq.enterprise.server.alert.AlertManagerBean.willDefinitionBeDisabled()
alertDefinition.setWillRecover(true);
dampeningCategory = AlertDampening.Category.NONE;
}
if (dampeningCategory == AlertDampening.Category.NO_DUPLICATES) {
dampeningCategory = AlertDampening.Category.NONE;
}
AlertDampening dampening = new AlertDampening(dampeningCategory);
if (adr.getDampeningCount()>-1) {
dampening.setValue(adr.getDampeningCount());
}
if (adr.getDampeningPeriod()>0) {
dampening.setPeriod(adr.getDampeningPeriod());
try {
if (adr.getDampeningUnit()!=null) {
dampening.setPeriodUnits(AlertDampening.TimeUnits.valueOf(adr.getDampeningUnit().toUpperCase()));
}
} catch (Exception e) {
throw new BadArgumentException("dampening unit", "Allowed values are MINUTES,HOURS,DAYS, WEEKS");
}
}
alertDefinition.setAlertDampening(dampening);
}
private AlertNotification notificationRestToNotification(AlertDefinition alertDefinition,
AlertNotificationRest anr) {
AlertNotification notification = new AlertNotification(anr.getSenderName());
if (notificationMgr.getAlertInfoForSender(anr.getSenderName()) == null) {
throw new StuffNotFoundException("AlertSender with name [" + anr.getSenderName() + "]");
}
notification.setAlertDefinition(alertDefinition);
notification.setConfiguration(ConfigurationHelper.mapToConfiguration(anr.getConfig()));
notification.setExtraConfiguration(ConfigurationHelper.mapToConfiguration(anr.getExtraConfig()));
return notification;
}
@DELETE
@Path("condition/{cid}")
@ApiOperation(value = "Remove an alert condition", notes = "This operation is by default idempotent, returning 204." +
"If you want to check if the condition existed at all, you need to pass the 'validate' query parameter.")
@ApiErrors({
@ApiError(code = 204, reason = "Condition was deleted or did not exist with validation not set"),
@ApiError(code = 404, reason = "Condition did not exist and validate was set")
})
public Response deleteCondition(
@ApiParam("The id of the condition to remove")@PathParam("cid") int conditionId,
@ApiParam("Validate if the condition exists") @QueryParam("validate") @DefaultValue("false") boolean validate) {
Integer definitionId;
try {
definitionId = findDefinitionIdForConditionId(conditionId);
}
catch (NoResultException nre) {
if (validate) {
throw new StuffNotFoundException("Condition with id " + conditionId);
}
else {
return Response.noContent().build();
}
}
AlertDefinition definition2;
definition2 = entityManager.find(AlertDefinition.class,definitionId);
AlertCondition condition=null;
for (AlertCondition c: definition2.getConditions()) {
if (c.getId() == conditionId) {
condition=c;
}
}
definition2.getConditions().remove(condition);
alertDefinitionManager.updateAlertDefinition(caller,definitionId,definition2,true);
return Response.noContent().build();
}
@POST
@Path("definition/{id}/conditions")
@ApiOperation(value = "Add a new alert condition to an existing alert definition",
notes = "<xml>" +
"<para>Each condition falls into a category. Allowed categories are " +
"AVAILABILITY, AVAIL_DURATION, BASELINE(m), CHANGE(m), CONTROL, DRIFT, EVENT, RANGE(m), RESOURCE_CONFIG, THRESHOLD(m), TRAIT(m)." +
"Categories with an appended (m) are for metrics and need a metricDefinition, but no name, as the name is obtained from the " +
"metric definition. Parameters vary depending on the category: " +
"<itemizedlist>"+
"<listitem><simpara>AVAILABILITY: name is one of AVAIL_GOES_DOWN, " +
"AVAIL_GOES_DISABLED, AVAIL_GOES_UNKNOWN, AVAIL_GOES_NOT_UP and AVAIL_GOES_UP.</simpara></listitem>" +
"<listitem><simpara>AVAIL_DURATION: name is one of AVAIL_DURATION_DOWN andAVAIL_DURATION_NOT_UP; option gives the duration in seconds.</simpara></listitem>"+
"<listitem><simpara>BASELINE: option is one of 'min','mean','max', threshold gives the percentage (0.01=1%), " +
"comparator is one of '<','=' and '>'.</simpara></listitem>" +
"<listitem><simpara>CONTROL: option gives the Operation status (FAILURE,SUCCESS,INPROGRESS,CANCELED), name is the name " +
"of the operation (not the display-name).</simpara></listitem>" +
"<listitem><simpara>EVENT: name is the severity (DEBUG,INFO,WARN,ERROR,FATAL), option is an optional RegEx to match against.</simpara></listitem>" +
"<listitem><simpara>DRIFT: name is optional and matches drift-definitions; option is optional and matches directories.</simpara></listitem>" +
"<listitem><simpara>RANGE: threshold has the lower bound, " +
"option the higher bound, comparator is one of '<','<=','=','>=' or '>'.</simpara></listitem>" +
"<listitem><simpara>RESOURCE_CONFIG: no additional params needed.</simpara></listitem>" +
"<listitem><simpara>THRESHOLD: comparator " +
"is one of '<','=','>'; threshold is the value to compare against.</simpara></listitem>" +
"<listitem><simpara>TRAIT: option is an optional RegEx to match against.</simpara></listitem>" +
"</itemizedlist>" +
"</para></xml>" )
@ApiErrors({
@ApiError(code = 404, reason = "No AlertDefinition with the passed id exists"),
@ApiError(code = 406, reason = "The passed condition failed validation. A more detailed message is provided"),
})
public Response addConditionToDefinition(
@ApiParam("The id of the alert definition") @PathParam("id") int definitionId,
@ApiParam("The condition to add") AlertConditionRest conditionRest, @Context UriInfo uriInfo) {
AlertDefinition definition = alertDefinitionManager.getAlertDefinition(caller,definitionId);
if (definition==null)
throw new StuffNotFoundException("AlertDefinition with id " + definitionId);
Resource resource = definition.getResource();
ResourceType resourceType = definition.getResourceType();
AlertCondition condition = conditionRestToCondition(conditionRest, resource, resourceType);
definition.addCondition(condition);
alertDefinitionManager.updateAlertDefinition(caller,definitionId,definition,false);
Response.ResponseBuilder builder = getResponseBuilderForCondition(definitionId, uriInfo, condition, true);
return builder.build();
}
private Integer findDefinitionIdForConditionId(int conditionId) {
/*
// this returns a proxy object, which is not fully initialized
// and all the further work will fail
AlertCondition condition = conditionMgr.getAlertConditionById(conditionId);
AlertDefinition def = condition.getAlertDefinition()
// So we need to "manually" pull that information in
*/
Query q = entityManager.createQuery("SELECT condition.alertDefinition.id FROM AlertCondition condition WHERE condition.id = :id ");
q.setParameter("id",conditionId);
Object o = q.getSingleResult();
return (Integer)o;
}
@PUT
@Path("condition/{cid}")
@ApiOperation("Update an existing condition of an alert definition.Note that the update will change the id of the condition")
@ApiErrors({
@ApiError(code = 404, reason = "Condition with passed id does not exist"),
@ApiError(code = 406, reason = "The passed category or condition operator was invalid")
})
public Response updateCondition(
@ApiParam("The id of the condition to update") @PathParam("cid") int conditionId,
@ApiParam("The updated condition") AlertConditionRest conditionRest, @Context UriInfo uriInfo) {
Integer definitionId;
try {
definitionId = findDefinitionIdForConditionId(conditionId);
}
catch (NoResultException nre) {
throw new StuffNotFoundException("Condition with id " + conditionId);
}
AlertDefinition definition = entityManager.find(AlertDefinition.class,definitionId);
AlertCondition condition=null;
for (Iterator<AlertCondition> iterator = definition.getConditions().iterator(); iterator.hasNext(); ) {
AlertCondition oldCondition = iterator.next();
if (oldCondition.getId() == conditionId) {
condition = new AlertCondition(oldCondition);
oldCondition.setAlertDefinition(null);
iterator.remove();
entityManager.remove(oldCondition);
}
}
Resource resource = definition.getResource();
ResourceType resourceType = definition.getResourceType();
AlertCondition restCondition = conditionRestToCondition(conditionRest, resource, resourceType);
condition.setOption(conditionRest.getOption());
condition.setComparator(conditionRest.getComparator());
condition.setMeasurementDefinition(restCondition.getMeasurementDefinition());
condition.setThreshold(conditionRest.getThreshold());
condition.setTriggerId(conditionRest.getTriggerId());
definition.getConditions().add(condition);
alertDefinitionManager.updateAlertDefinitionInternal(caller, definitionId, definition, true, true, true);
entityManager.flush();
Response.ResponseBuilder builder = getResponseBuilderForCondition(definitionId,uriInfo,condition,false);
return builder.build();
}
@GET
@Path("condition/{cid}")
@ApiOperation("Retrieve a condition of an alert definition by its condition id")
@ApiError(code = 404, reason = "No condition with the passed id exists")
public Response getCondition(
@ApiParam("The id of the condition to retrieve") @PathParam("cid") int conditionId) {
AlertCondition condition = conditionMgr.getAlertConditionById(conditionId);
if (condition==null) {
throw new StuffNotFoundException("No condition with id " + conditionId);
}
AlertConditionRest acr = conditionToConditionRest(condition);
return Response.ok(acr).build();
}
/**
* Convert a passed condition from the REST side into the internal domain
* representation. The largest part of this method is validation of the input
*
*
* @param conditionRest Object to convert
* @param resource Optional {@link org.rhq.core.domain.resource.Resource} to check against if not null
* @param resourceType Optional {@link ResourceType} to validate against if not null
* @return Converted domain object
* @throws BadArgumentException If validation fails
*/
private AlertCondition conditionRestToCondition(AlertConditionRest conditionRest, Resource resource,
ResourceType resourceType) {
AlertCondition condition = new AlertCondition();
try {
condition.setCategory(AlertConditionCategory.valueOf(conditionRest.getCategory().toUpperCase()));
} catch (Exception e) {
String allowedValues = stringify(AlertConditionCategory.class);
throw new BadArgumentException("Field 'category' [" + conditionRest.getCategory() + "] is invalid. Allowed values "+
"are : " + allowedValues);
}
int measurementDefinition = conditionRest.getMeasurementDefinition();
MeasurementDefinition md;
if (measurementDefinition!=0) {
md = entityManager.find(MeasurementDefinition.class, measurementDefinition);
if (md==null) {
throw new StuffNotFoundException("measurementDefinition with id " + measurementDefinition);
}
// Validate that the definition belongs to the resource, if passed
if (resource!=null) {
ResourceType type = resource.getResourceType();
Set<MeasurementDefinition> definitions = type.getMetricDefinitions();
if (!definitions.contains(md)) {
throw new BadArgumentException("MeasurementDefinition does not apply to resource");
}
}
// Validate that the definition belongs to the passed resource type
if (resourceType!=null) {
Set<MeasurementDefinition> definitions = resourceType.getMetricDefinitions();
if (!definitions.contains(md)) {
throw new BadArgumentException("MeasurementDefinition does not apply to resource type");
}
}
}
String optionValue = conditionRest.getOption();
String conditionName = conditionRest.getName();
// Set the name for all cases and allow it to be overridden later.
condition.setName(conditionName);
AlertConditionCategory category = condition.getCategory();
switch (category) {
case ALERT:
// Looks internal -- noting to do.
break;
case AVAIL_DURATION:
if (optionValue ==null) {
throw new BadArgumentException("Option needs to be provided as duration in seconds");
}
try {
Integer.parseInt(optionValue);
} catch (NumberFormatException nfe) {
throw new BadArgumentException("Option provided [" + optionValue + "] was bad. Must be duration in seconds");
}
checkForAllowedValues("name", conditionName, "AVAIL_DURATION_DOWN", "AVAIL_DURATION_NOT_UP");
break;
case AVAILABILITY:
checkForAllowedValues("name", conditionName, "AVAIL_GOES_DOWN", "AVAIL_GOES_DISABLED",
"AVAIL_GOES_UNKNOWN", "AVAIL_GOES_NOT_UP", "AVAIL_GOES_UP");
break;
case BASELINE:
if (measurementDefinition ==0) {
throw new BadArgumentException("You need to provide a measurementDefinition for category BASELINE");
}
md = entityManager.find(MeasurementDefinition.class,
measurementDefinition);
if (md==null) {
throw new StuffNotFoundException("measurementDefinition with id " + measurementDefinition);
}
condition.setMeasurementDefinition(md);
condition.setName(md.getDisplayName());
checkForAllowedValues("option", optionValue, "min", "max", "mean");
checkForAllowedValues("comparator", conditionRest.getComparator(), "<", "=", ">");
break;
case CHANGE:
md = getMeasurementDefinition(measurementDefinition, category);
condition.setMeasurementDefinition(md);
condition.setName(md.getDisplayName());
if (md.getDataType()== DataType.CALLTIME) {
checkForAllowedValues("option", optionValue, "MIN", "MAX", "AVG");
}
break;
case CONTROL:
checkForAllowedValues("option",optionValue,"INPROGRESS", "SUCCESS", "FAILURE", "CANCELED");
if (conditionName ==null) {
throw new BadArgumentException("name must be the name (not display name) of a valid operation.");
}
// TODO check for valid operation -- only on the resource or type itself (still hard enough)
break;
case DRIFT:
// option and name are optional, so nothing to do
break;
case EVENT:
checkForAllowedValues("name", conditionName,"DEBUG", "INFO", "WARN", "ERROR", "FATAL");
// option is an optional regular expression
break;
case RANGE:
checkForAllowedValues("comparator", conditionRest.getComparator(), "<", "=", ">","<=",">=");
if (optionValue==null) {
throw new BadArgumentException("You need to supply an upper threshold in 'option' as numeric value");
}
try {
Double.parseDouble(optionValue);
}
catch (NumberFormatException nfe) {
throw new BadArgumentException("You need to supply an upper threshold in 'option' as numeric value");
}
md = getMeasurementDefinition(measurementDefinition, category);
condition.setMeasurementDefinition(md);
condition.setName(md.getDisplayName());
break;
case RESOURCE_CONFIG:
// Nothing to do
break;
case THRESHOLD:
checkForAllowedValues("comparator", conditionRest.getComparator(), "<", "=", ">");
md = getMeasurementDefinition(measurementDefinition, category);
condition.setMeasurementDefinition(md);
condition.setName(md.getDisplayName());
if (md.getDataType()== DataType.CALLTIME) {
checkForAllowedValues("option", optionValue, "MIN", "MAX", "AVG");
}
break;
case TRAIT:
md = getMeasurementDefinition(measurementDefinition, category);
condition.setMeasurementDefinition(md);
condition.setName(md.getDisplayName());
// No need to check options - they are optional
break;
}
condition.setOption(optionValue);
condition.setComparator(conditionRest.getComparator());
condition.setThreshold(conditionRest.getThreshold());
condition.setTriggerId(conditionRest.getTriggerId());
return condition;
}
private MeasurementDefinition getMeasurementDefinition(int measurementDefinition, AlertConditionCategory category) {
MeasurementDefinition md;
if (measurementDefinition ==0) {
throw new BadArgumentException("You need to provide a measurementDefinition for category " + category.name());
}
md = entityManager.find(MeasurementDefinition.class,
measurementDefinition);
if (md==null) {
throw new StuffNotFoundException("measurementDefinition with id " + measurementDefinition);
}
return md;
}
/**
* Test if #toCheck matches one of the allowedValues and throw a BadArgumentException
* if not. In this case the attribute name is passed in the exception
*
*
* @param attributeName Name of the Attribute in error
* @param toCheck Value to check for
* @param allowedValues Allowed values
* @throws BadArgumentException if the values to check does not match any of the allowed values
*/
private void checkForAllowedValues(String attributeName, String toCheck, String... allowedValues) {
if (toCheck==null) {
throw new BadArgumentException("Field " + attributeName + " must be set. Allowed values are: " + StringUtil.arrayToString(allowedValues));
}
if (allowedValues==null) {
throw new IllegalArgumentException("No allowed values are provided - please contact support");
}
boolean match = false;
for (String value : allowedValues) {
if (toCheck.equals(value))
match=true;
}
if (!match) {
throw new BadArgumentException("Field " + attributeName + " has an invalid value [" + toCheck + "]. Allowed values are: "
+ StringUtil.arrayToString(allowedValues));
}
}
/**
* List the names of the passed Enum as a comma separated String
* @param clazz
* @return
*/
private String stringify(Class<? extends Enum> clazz) {
EnumSet enumSet = EnumSet.allOf(clazz);
StringBuilder b = new StringBuilder();
Iterator iter = enumSet.iterator();
while (iter.hasNext()) {
Enum anEnum= (Enum) iter.next();
b.append(anEnum.name());
if (iter.hasNext()) {
b.append(", ");
}
}
return b.toString();
}
private Response.ResponseBuilder getResponseBuilderForCondition(int definitionId, UriInfo uriInfo,
AlertCondition originalCondition, boolean isCreate) {
AlertDefinition updatedDefinition = alertDefinitionManager.getAlertDefinition(caller,definitionId);
Set<AlertCondition> conditions = updatedDefinition.getConditions();
int conditionId=-1;
AlertCondition createdCondition = null;
for (AlertCondition cond :conditions) {
if (originalCondition.getId() == cond.getId() || (
cond.getName() != null && cond.getName().equals(originalCondition.getName())))
{
conditionId = cond.getId();
createdCondition = cond;
}
}
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/alert/condition/{cid}");
URI uri = uriBuilder.build(conditionId);
AlertConditionRest result = conditionToConditionRest(createdCondition);
Response.ResponseBuilder builder;
if (isCreate) {
builder = Response.created(uri);
}
else {
builder = Response.ok();
builder.location(uri);
}
builder.entity(result);
return builder;
}
@GET
@Path("notification/{nid}")
@ApiOperation("Return a notification definition by its id")
@ApiError(code = 404, reason = "No notification with the passed id found")
public Response getNotification(
@ApiParam("The id of the notification definition to retrieve") @PathParam("nid") int notificationId) {
AlertNotification notification = notificationMgr.getAlertNotification(caller,notificationId);
if (notification==null) {
throw new StuffNotFoundException("No notification with id " + notificationId);
}
AlertNotificationRest anr = notificationToNotificationRest(notification);
return Response.ok(anr).build();
}
@DELETE
@Path("notification/{nid}")
@ApiOperation(value = "Remove a notification definition", notes = "This operation is by default idempotent, returning 204." +
"If you want to check if the notification existed at all, you need to pass the 'validate' query parameter.")
@ApiErrors({
@ApiError(code = 204, reason = "Notification was deleted or did not exist with validation not set"),
@ApiError(code = 404, reason = "Notification did not exist and validate was set")
})
public Response deleteNotification(
@ApiParam("The id of the notification definition to remove") @PathParam("nid") int notificationId,
@ApiParam("Validate if the notification exists") @QueryParam("validate") @DefaultValue("false") boolean validate) {
AlertNotification notification = notificationMgr.getAlertNotification(caller,notificationId);
if (notification!=null) {
AlertDefinition definition = alertDefinitionManager.getAlertDefinition(caller,notification.getAlertDefinition().getId());
definition.getAlertNotifications().remove(notification);
alertDefinitionManager.updateAlertDefinitionInternal(caller,definition.getId(),definition,true,true,true);
entityManager.flush();
} else {
if (validate) {
throw new StuffNotFoundException("Notification with id "+ notificationId);
}
}
return Response.noContent().build();
}
@PUT
@Path("notification/{nid}")
@ApiOperation("Update a notification definition")
@ApiError(code = 404, reason = "There is no notification with the passed id")
public Response updateNotification(
@ApiParam("The id of the notification definition to update") @PathParam("nid") int notificationId,
@ApiParam("The updated notification definition to use") AlertNotificationRest notificationRest) {
AlertNotification notification = notificationMgr.getAlertNotification(caller,notificationId);
if (notification==null) {
throw new StuffNotFoundException("No notification with id " + notificationId);
}
AlertDefinition definition = alertDefinitionManager.getAlertDefinition(caller,notification.getAlertDefinition().getId());
AlertNotification newNotif = notificationRestToNotification(definition,notificationRest);
notification.setConfiguration(newNotif.getConfiguration());
notification.setExtraConfiguration(newNotif.getExtraConfiguration());
// id and sender need to stay the same
alertDefinitionManager.updateAlertDefinitionInternal(caller,definition.getId(),definition,true,true,true);
entityManager.flush();
List<AlertNotification> notifications = definition.getAlertNotifications();
int newNotifId = 0;
for (AlertNotification n : notifications) {
if (n.getSenderName().equals(notification.getSenderName())) {
newNotifId = n.getId();
}
}
AlertNotification result = notificationMgr.getAlertNotification(caller,newNotifId);
AlertNotificationRest resultRest = notificationToNotificationRest(result);
return Response.ok(resultRest).build(); // TODO
}
@POST
@Path("definition/{id}/notifications")
@ApiOperation("Add a new notification definition to an alert definition")
@ApiErrors({
@ApiError(code = 404, reason = "Requested alert notification sender does not exist"),
@ApiError(code = 404, reason = "There is no alert definition with the passed id")
})
public Response addNotificationToDefinition(
@ApiParam("Id of the alert definition that should get the notification definition") @PathParam("id") int definitionId,
@ApiParam("The notification definition to add") AlertNotificationRest notificationRest, @Context UriInfo uriInfo) {
// Now check if the definition exists as well
AlertDefinition definition = alertDefinitionManager.getAlertDefinition(caller,definitionId);
if (definition==null) {
throw new StuffNotFoundException("AlertDefinition with id " + definitionId);
}
AlertNotification notification = notificationRestToNotification(definition, notificationRest);
// definition and sender are valid, continue
int existingNotificationCount = definition.getAlertNotifications().size();
// notification.setAlertDefinition(definition); setting this will result in duplicated notifications
definition.addAlertNotification(notification);
alertDefinitionManager.updateAlertDefinitionInternal(caller, definitionId, definition, false, true, true);
alertDefinitionManager.getAlertDefinition(caller,definitionId);
entityManager.flush();
AlertDefinition updatedDefinition = alertDefinitionManager.getAlertDefinitionById(caller,definitionId);
List<AlertNotification> notifs = updatedDefinition.getAlertNotifications();
assert notifs.size() == existingNotificationCount +1;
AlertNotification updatedNotification = notifs.get(existingNotificationCount);
AlertNotificationRest updatedNotificationRest = notificationToNotificationRest(updatedNotification);
int notificationId = updatedNotification.getId();
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/alert/notification/{nid}");
URI uri = uriBuilder.build(notificationId );
Response.ResponseBuilder builder = Response.created(uri);
builder.entity(updatedNotificationRest);
return builder.build();
}
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@ApiOperation("Return a list of alert notification senders with a short description. The list does not include the configuration definition.")
@GET @GZIP
@Path("senders")
public Response getAlertSenders(@Context UriInfo uriInfo) {
List<String> senderNames = notificationMgr.listAllAlertSenders();
List<AlertSender> senderList = new ArrayList<AlertSender>(senderNames.size());
for (String senderName : senderNames) {
AlertSenderInfo info = notificationMgr.getAlertInfoForSender(senderName);
AlertSender sender = new AlertSender(senderName);
sender.setDescription(info.getDescription());
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/alert/sender/{name}");
URI uri = uriBuilder.build(sender.getSenderName());
Link self = new Link("self",uri.toString());
sender.setLink(self);
senderList.add(sender);
}
GenericEntity<List<AlertSender>> entity = new GenericEntity<List<AlertSender>>(senderList) {};
Response.ResponseBuilder builder = Response.ok(entity);
return builder.build();
}
@Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
@ApiOperation("Return an alert notification sender by name. This includes information about the configuration it expects")
@GET @GZIP
@Path("sender/{name}")
@ApiError(code = 404, reason = "There is no sender with the passed name")
public Response getAlertSenderByName(
@ApiParam("Name of the sender to retrieve") @PathParam("name")String senderName, @Context UriInfo uriInfo) {
AlertSenderInfo info = notificationMgr.getAlertInfoForSender(senderName);
if (info==null) {
throw new StuffNotFoundException("Alert sender with name [" + senderName + "]");
}
AlertSender sender = new AlertSender(senderName);
sender.setDescription(info.getDescription());
ConfigurationDefinition definition = notificationMgr.getConfigurationDefinitionForSender(senderName);
for (PropertyDefinition pd : definition.getPropertyDefinitions().values()) {
if (pd instanceof PropertyDefinitionSimple) {
PropertyDefinitionSimple pds = (PropertyDefinitionSimple) pd;
sender.getConfigDefinition().put(pds.getName(),pds.getType().name());
}
else {
log.warn("Property " + pd.getName() + " for sender " + senderName + " is not of a supported type");
}
}
UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
uriBuilder.path("/alert/sender/{name}");
URI uri = uriBuilder.build(sender.getSenderName());
Link self = new Link("self",uri.toString());
sender.setLink(self);
Response.ResponseBuilder builder = Response.ok(sender);
return builder.build();
}
AlertDefinitionRest definitionToDomain(AlertDefinition def, boolean full, UriInfo uriInfo) {
AlertDefinitionRest adr = new AlertDefinitionRest(def.getId());
adr.setName(def.getName());
adr.setEnabled(def.getEnabled());
adr.setPriority(def.getPriority().getName());
adr.setConditionMode(def.getConditionExpression().toString());
adr.setRecoveryId(def.getRecoveryId());
adr.setDeleted(def.getDeleted());
if (full) {
Set<AlertCondition> conditions = def.getConditions();
if (conditions.size() > 0) {
List<AlertConditionRest> conditionRestList = new ArrayList<AlertConditionRest>(conditions.size());
for (AlertCondition condition : conditions) {
AlertConditionRest acr = conditionToConditionRest(condition);
conditionRestList.add(acr);
}
adr.setConditions(conditionRestList);
}
List<AlertNotification> notifications = def.getAlertNotifications();
if (notifications.size() > 0) {
List<AlertNotificationRest> notificationRestList = new ArrayList<AlertNotificationRest>(notifications.size());
for (AlertNotification notification : notifications) {
AlertNotificationRest anr = notificationToNotificationRest(notification);
notificationRestList.add(anr);
}
adr.setNotifications(notificationRestList);
}
}
AlertDampening dampening = def.getAlertDampening();
adr.setDampeningCategory(dampening.getCategory().name());
if (dampening.getCategory()== AlertDampening.Category.NONE && def.getWillRecover()) {
adr.setDampeningCategory(AlertDampening.Category.ONCE.name());
}
AlertDampening.TimeUnits units = dampening.getValueUnits();
String s = units != null ? " " + units.name() : "";
adr.setDampeningCount(dampening.getValue());
units = dampening.getPeriodUnits();
s = units != null ? " " + units.name() : "";
adr.setDampeningPeriod(dampening.getPeriod());
if (dampening.getPeriodUnits()!=null) {
adr.setDampeningUnit(dampening.getPeriodUnits().name());
}
List<Link> links = adr.getLinks();
if (def.getResource()!=null) {
links.add(createUILink(uriInfo, UILinkTemplate.RESOURCE_ALERT_DEF, def.getResource().getId(), adr.getId()));
links.add(getLinkToResource(def.getResource(), uriInfo, "resource"));
} else if (def.getGroup() != null) {
links.add(
createUILink(uriInfo, UILinkTemplate.GROUP_ALERT_DEF, def.getGroup().getId(), adr.getId()));
links.add(getLinkToGroup(def.getGroup(), uriInfo, "group"));
} else {
links.add(
createUILink(uriInfo, UILinkTemplate.TEMPLATE_ALERT_DEF, def.getResourceType().getId(), adr.getId()));
links.add(getLinkToResourceType(def.getResourceType(),uriInfo,"resourceType"));
}
return adr;
}
private AlertNotificationRest notificationToNotificationRest(AlertNotification notification) {
AlertNotificationRest anr = new AlertNotificationRest();
anr.setId(notification.getId());
anr.setSenderName(notification.getSenderName());
ConfigurationDefinition configDef = notificationMgr.getConfigurationDefinitionForSender(notification
.getSenderName());
anr.setConfig(ConfigurationHelper.configurationToMap(notification.getConfiguration(), configDef, false));
ConfigurationDefinition extraConfigDef = null;
if ("Resource Operations".equals(notification.getSenderName())) {
OperationDefinition opDef = operationMgr.getOperationDefinition(caller,
Integer.valueOf(notification.getConfiguration().getSimpleValue("operation-definition-id", "0")));
extraConfigDef = opDef.getParametersConfigurationDefinition();
}
anr.setExtraConfig(ConfigurationHelper.configurationToMap(notification.getExtraConfiguration(), extraConfigDef,
false));
return anr;
}
private AlertConditionRest conditionToConditionRest(AlertCondition condition) {
AlertConditionRest acr = new AlertConditionRest();
acr.setId(condition.getId());
acr.setName(condition.getName());
acr.setCategory(condition.getCategory().getName());
acr.setOption(condition.getOption());
acr.setComparator(condition.getComparator());
acr.setMeasurementDefinition(condition.getMeasurementDefinition()==null?0:condition.getMeasurementDefinition().getId());
acr.setThreshold(condition.getThreshold());
acr.setTriggerId(condition.getTriggerId()); // TODO what's that?
return acr;
}
}