/*
* Copyright (c) 2016 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.api.service.impl.resource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.api.mapper.DbObjectMapper;
import com.emc.storageos.api.mapper.EventMapper;
import com.emc.storageos.api.mapper.functions.MapEvent;
import com.emc.storageos.api.service.impl.response.BulkList;
import com.emc.storageos.api.service.impl.response.RestLinkFactory;
import com.emc.storageos.computesystemcontroller.ComputeSystemController;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.db.client.constraint.AggregatedConstraint;
import com.emc.storageos.db.client.constraint.AggregationQueryResultList;
import com.emc.storageos.db.client.constraint.Constraint;
import com.emc.storageos.db.client.model.ActionableEvent;
import com.emc.storageos.db.client.model.DataObject;
import com.emc.storageos.db.client.model.StringSet;
import com.emc.storageos.db.client.model.TenantOrg;
import com.emc.storageos.db.client.model.util.EventUtils;
import com.emc.storageos.db.exceptions.DatabaseException;
import com.emc.storageos.model.BulkIdParam;
import com.emc.storageos.model.ResourceTypeEnum;
import com.emc.storageos.model.RestLinkRep;
import com.emc.storageos.model.TaskList;
import com.emc.storageos.model.TaskResourceRep;
import com.emc.storageos.model.event.EventBulkRep;
import com.emc.storageos.model.event.EventDetailsRestRep;
import com.emc.storageos.model.event.EventList;
import com.emc.storageos.model.event.EventRestRep;
import com.emc.storageos.model.event.EventStatsRestRep;
import com.emc.storageos.model.search.SearchResultResourceRep;
import com.emc.storageos.model.search.SearchResults;
import com.emc.storageos.security.authentication.StorageOSUser;
import com.emc.storageos.security.authorization.ACL;
import com.emc.storageos.security.authorization.CheckPermission;
import com.emc.storageos.security.authorization.DefaultPermissions;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.google.common.collect.Lists;
/**
* A service that provides APIs for viewing, approving, declining and removing actionable events.
*/
@DefaultPermissions(readRoles = { Role.TENANT_ADMIN, Role.SYSTEM_MONITOR, Role.SYSTEM_ADMIN }, writeRoles = {
Role.TENANT_ADMIN }, readAcls = { ACL.ANY })
@Path("/vdc/events")
public class EventService extends TaggedResource {
protected final static Logger _log = LoggerFactory.getLogger(EventService.class);
private static final String EVENT_SERVICE_TYPE = "event";
private static final String TENANT_QUERY_PARAM = "tenant";
private static final String RESOURCE_QUERY_PARAM = "resource";
private static final String DETAILS_SUFFIX = "Details";
@Override
public String getServiceType() {
return EVENT_SERVICE_TYPE;
}
@GET
@Path("/{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public EventRestRep getEvent(@PathParam("id") URI id) throws DatabaseException {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
// check the user permissions
verifyAuthorizedInTenantOrg(event.getTenant(), getUserFromContext());
return EventMapper.map(event);
}
@POST
@Path("/{id}/deactivate")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.TENANT_ADMIN })
public Response deleteEvent(@PathParam("id") URI id) throws DatabaseException {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
// check the user permissions
verifyAuthorizedInTenantOrg(event.getTenant(), getUserFromContext());
_dbClient.markForDeletion(event);
_log.info(
"Deleting Actionable Event: " + event.getId() + " Tenant: " + event.getTenant() + " Description: " + event.getDescription()
+ " Warning: " + event.getWarning()
+ " Event Status: " + event.getEventStatus() + " Resource: " + event.getResource() + " Event Code: "
+ event.getEventCode());
return Response.ok().build();
}
@POST
@Path("/{id}/approve")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.TENANT_ADMIN })
public TaskList approveEvent(@PathParam("id") URI id) throws DatabaseException {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
verifyAuthorizedInTenantOrg(event.getTenant(), getUserFromContext());
if (!StringUtils.equalsIgnoreCase(event.getEventStatus(), ActionableEvent.Status.pending.name())
&& !StringUtils.equalsIgnoreCase(event.getEventStatus(), ActionableEvent.Status.failed.name())) {
throw APIException.badRequests.eventCannotBeApproved(event.getEventStatus());
}
_log.info(
"Approving Actionable Event: " + event.getId() + " Tenant: " + event.getTenant() + " Description: " + event.getDescription()
+ " Warning: " + event.getWarning()
+ " Event Status: " + event.getEventStatus() + " Resource: " + event.getResource() + " Event Code: "
+ event.getEventCode());
return executeEventMethod(event, true);
}
@GET
@Path("/{id}/details")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public EventDetailsRestRep eventDetails(@PathParam("id") URI id) throws DatabaseException {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
verifyAuthorizedInTenantOrg(event.getTenant(), getUserFromContext());
EventDetailsRestRep eventDetails = new EventDetailsRestRep();
eventDetails.setApproveDetails(getEventDetails(event, true));
eventDetails.setDeclineDetails(getEventDetails(event, false));
return eventDetails;
}
/**
* Gets details for an event
*
* @param event the event to get details for
* @param approve if true, get the approve details, if false the get the decline details
* @return event details
*/
public List<String> getEventDetails(ActionableEvent event, boolean approve) {
byte[] method = approve ? event.getApproveMethod() : event.getDeclineMethod();
if (method == null || method.length == 0) {
_log.info("Method is null or empty for event " + event.getId());
return Lists.newArrayList("N/A");
}
ActionableEvent.Method eventMethod = ActionableEvent.Method.deserialize(method);
if (eventMethod == null) {
_log.info("Event method is null or empty for event " + event.getId());
return Lists.newArrayList("N/A");
}
String eventMethodName = eventMethod.getMethodName() + DETAILS_SUFFIX;
try {
Method classMethod = getMethod(ActionableEventExecutor.class, eventMethodName);
if (classMethod == null) {
return Lists.newArrayList("N/A");
} else {
ComputeSystemController controller = getController(ComputeSystemController.class, null);
ActionableEventExecutor executor = new ActionableEventExecutor(_dbClient, controller);
return (List<String>) classMethod.invoke(executor, eventMethod.getArgs());
}
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
_log.error(e.getMessage(), e.getCause());
throw APIException.badRequests.errorInvokingEventMethod(event.getId(), eventMethodName);
}
}
/**
* Executes an actionable event method
*
* @param event the event to execute
* @param approve if true, the action is to approve, if false the action is to decline
* @return list of tasks
*/
public TaskList executeEventMethod(ActionableEvent event, boolean approve) {
TaskList taskList = new TaskList();
byte[] method = approve ? event.getApproveMethod() : event.getDeclineMethod();
String eventStatus = approve ? ActionableEvent.Status.approved.name() : ActionableEvent.Status.declined.name();
event.setEventExecutionTime(Calendar.getInstance());
event.setApproveDetails(new StringSet(getEventDetails(event, true)));
event.setDeclineDetails(new StringSet(getEventDetails(event, false)));
if (method == null || method.length == 0) {
_log.info("Method is null or empty for event " + event.getId());
event.setEventStatus(eventStatus);
_dbClient.updateObject(event);
return taskList;
}
ActionableEvent.Method eventMethod = ActionableEvent.Method.deserialize(method);
if (eventMethod == null) {
_log.info("Event method is null or empty for event " + event.getId());
event.setEventStatus(eventStatus);
_dbClient.updateObject(event);
return taskList;
}
try {
Method classMethod = getMethod(ActionableEventExecutor.class, eventMethod.getMethodName());
ComputeSystemController controller = getController(ComputeSystemController.class, null);
ActionableEventExecutor executor = new ActionableEventExecutor(_dbClient, controller);
Object[] parameters = Arrays.copyOf(eventMethod.getArgs(), eventMethod.getArgs().length + 1);
parameters[parameters.length - 1] = event.getId();
event.setEventStatus(eventStatus);
_dbClient.updateObject(event);
TaskResourceRep result = (TaskResourceRep) classMethod.invoke(executor, parameters);
if (result != null && result.getId() != null) {
Collection<String> taskCollection = Lists.newArrayList(result.getId().toString());
ActionableEvent updatedEvent = _dbClient.queryObject(ActionableEvent.class, event.getId());
updatedEvent.setTaskIds(new StringSet(taskCollection));
_dbClient.updateObject(updatedEvent);
}
taskList.addTask(result);
return taskList;
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
_log.error(e.getMessage(), e.getCause());
throw APIException.badRequests.errorInvokingEventMethod(event.getId(), eventMethod.getMethodName());
}
}
/**
* Returns a reference to a method for the given class with the given name
*
* @param clazz class which the method belongs
* @param name the name of the method
* @return method or null if it doesn't exist
*/
private Method getMethod(Class clazz, String name) {
for (Method method : clazz.getMethods()) {
if (method.getName().equalsIgnoreCase(name)) {
return method;
}
}
return null;
}
@POST
@Path("/{id}/decline")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.TENANT_ADMIN })
public TaskList declineEvent(@PathParam("id") URI id) throws DatabaseException {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
verifyAuthorizedInTenantOrg(event.getTenant(), getUserFromContext());
if (!StringUtils.equalsIgnoreCase(event.getEventStatus(), ActionableEvent.Status.pending.name())
&& !StringUtils.equalsIgnoreCase(event.getEventStatus(), ActionableEvent.Status.failed.name())) {
throw APIException.badRequests.eventCannotBeDeclined(event.getEventStatus());
}
_log.info(
"Declining Actionable Event: " + event.getId() + " Tenant: " + event.getTenant() + " Description: " + event.getDescription()
+ " Warning: " + event.getWarning()
+ " Event Status: " + event.getEventStatus() + " Resource: " + event.getResource() + " Event Code: "
+ event.getEventCode());
return executeEventMethod(event, false);
}
/**
* Retrieve resource representations based on input ids.
*
* @param param POST data containing the id list.
* @brief List data of event resources
* @return list of representations.
*
* @throws DatabaseException When an error occurs querying the database.
*/
@POST
@Path("/bulk")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public EventBulkRep getBulkResources(BulkIdParam param) {
return (EventBulkRep) super.getBulkResources(param);
}
@SuppressWarnings("unchecked")
@Override
public Class<ActionableEvent> getResourceClass() {
return ActionableEvent.class;
}
@Override
public EventBulkRep queryBulkResourceReps(List<URI> ids) {
Iterator<ActionableEvent> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids);
return new EventBulkRep(BulkList.wrapping(_dbIterator, MapEvent.getInstance()));
}
@Override
public EventBulkRep queryFilteredBulkResourceReps(List<URI> ids) {
Iterator<ActionableEvent> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids);
BulkList.ResourceFilter filter = new BulkList.EventFilter(getUserFromContext(), _permissionsHelper);
return new EventBulkRep(BulkList.wrapping(_dbIterator, MapEvent.getInstance(), filter));
}
@Override
protected boolean isZoneLevelResource() {
return false;
}
@Override
protected boolean isSysAdminReadableResource() {
return true;
}
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public EventList listEvents(@QueryParam("tenant") final URI tid) throws DatabaseException {
URI tenantId;
StorageOSUser user = getUserFromContext();
if (tid == null || StringUtils.isBlank(tid.toString())) {
tenantId = URI.create(user.getTenantId());
} else {
tenantId = tid;
}
// this call validates the tenant id
TenantOrg tenant = _permissionsHelper.getObjectById(tenantId, TenantOrg.class);
ArgValidator.checkEntity(tenant, tenantId, isIdEmbeddedInURL(tenantId), true);
// check the user permissions for this tenant org
verifyAuthorizedInTenantOrg(tenantId, user);
// get all host children
EventList list = new EventList();
list.setEvents(DbObjectMapper.map(ResourceTypeEnum.EVENT, listChildren(tenantId, ActionableEvent.class, "label", "tenant")));
return list;
}
@GET
@Path("/stats")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public EventStatsRestRep getStats(@QueryParam(TENANT_QUERY_PARAM) URI tenantId) {
verifyAuthorizedInTenantOrg(tenantId, getUserFromContext());
int approved = 0;
int declined = 0;
int pending = 0;
int failed = 0;
Constraint constraint = AggregatedConstraint.Factory.getAggregationConstraint(ActionableEvent.class, "tenant",
tenantId.toString(), "eventStatus");
AggregationQueryResultList queryResults = new AggregationQueryResultList();
_dbClient.queryByConstraint(constraint, queryResults);
Iterator<AggregationQueryResultList.AggregatedEntry> it = queryResults.iterator();
while (it.hasNext()) {
AggregationQueryResultList.AggregatedEntry entry = it.next();
if (entry.getValue().equals(ActionableEvent.Status.approved.name())) {
approved++;
} else if (entry.getValue().equals(ActionableEvent.Status.declined.name())) {
declined++;
} else if (entry.getValue().equals(ActionableEvent.Status.failed.name())) {
failed++;
} else {
pending++;
}
}
return new EventStatsRestRep(pending, approved, declined, failed);
}
@Override
protected SearchResults getOtherSearchResults(Map<String, List<String>> parameters, boolean authorized) {
SearchResults searchResults = new SearchResults();
if (parameters.containsKey(RESOURCE_QUERY_PARAM)) {
URI resourceId = URI.create(parameters.get(RESOURCE_QUERY_PARAM).get(0));
List<ActionableEvent> events = EventUtils.findResourceEvents(_dbClient, resourceId);
searchResults.getResource().addAll(toSearchResults(events));
}
return searchResults;
}
private List<SearchResultResourceRep> toSearchResults(List<ActionableEvent> items) {
List<SearchResultResourceRep> results = Lists.newArrayList();
for (ActionableEvent item : items) {
results.add(toSearchResult(item.getId()));
}
return results;
}
private SearchResultResourceRep toSearchResult(URI uri) {
RestLinkRep selfLink = new RestLinkRep("self", RestLinkFactory.newLink(getResourceType(), uri));
return new SearchResultResourceRep(uri, selfLink, null);
}
protected ActionableEvent queryEvent(DbClient dbClient, URI id) throws DatabaseException {
return queryObject(ActionableEvent.class, id, false);
}
@Override
protected DataObject queryResource(URI id) {
return queryObject(ActionableEvent.class, id, false);
}
@Override
protected URI getTenantOwner(URI id) {
ActionableEvent event = queryObject(ActionableEvent.class, id, false);
return event.getTenant();
}
@Override
protected ResourceTypeEnum getResourceType() {
return ResourceTypeEnum.EVENT;
}
}