/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.api.service.impl.resource;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.Consumes;
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.MediaType;
import javax.ws.rs.core.Response;
import com.emc.storageos.api.mapper.TaskMapper;
import com.emc.storageos.api.mapper.functions.MapTask;
import com.emc.storageos.api.service.authorization.PermissionsHelper;
import com.emc.storageos.api.service.impl.response.BulkList;
import com.emc.storageos.api.service.impl.response.ResRepFilter;
import com.emc.storageos.api.service.impl.response.RestLinkFactory;
import com.emc.storageos.db.client.TimestampedURIQueryResult;
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.constraint.ContainmentConstraint;
import com.emc.storageos.db.client.model.NamedURI;
import com.emc.storageos.db.client.model.Operation;
import com.emc.storageos.db.client.model.Task;
import com.emc.storageos.db.client.model.TenantOrg;
import com.emc.storageos.db.client.model.Workflow;
import com.emc.storageos.db.client.model.util.TaskUtils;
import com.emc.storageos.model.BulkIdParam;
import com.emc.storageos.model.BulkRestRep;
import com.emc.storageos.model.NamedRelatedResourceRep;
import com.emc.storageos.model.RelatedResourceRep;
import com.emc.storageos.model.ResourceOperationTypeEnum;
import com.emc.storageos.model.ResourceTypeEnum;
import com.emc.storageos.model.RestLinkRep;
import com.emc.storageos.model.TagAssignment;
import com.emc.storageos.model.TaskResourceRep;
import com.emc.storageos.model.search.SearchResultResourceRep;
import com.emc.storageos.model.search.SearchResults;
import com.emc.storageos.model.search.Tags;
import com.emc.storageos.model.tasks.TaskBulkRep;
import com.emc.storageos.model.tasks.TaskStatsRestRep;
import com.emc.storageos.model.tasks.TasksList;
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.services.OperationTypeEnum;
import com.emc.storageos.services.util.TimeUtils;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.workflow.WorkflowController;
import com.emc.storageos.workflow.WorkflowState;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@Path("/vdc/tasks")
@DefaultPermissions(readRoles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN, Role.SYSTEM_MONITOR, Role.TENANT_ADMIN }, writeRoles = {
Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN, Role.TENANT_ADMIN })
public class TaskService extends TaggedResource {
private static final URI SYSTEM_TENANT = URI.create("system");
private static final Integer FETCH_ALL = -1;
private static final String TENANT_QUERY_PARAM = "tenant";
private static final String RESOURCE_QUERY_PARAM = "resource";
private static final String MAX_COUNT_PARAM = "max_count";
private static final String START_TIME = "startTime";
private static final String END_TIME = "endTime";
private static final String STATE_PARAM = "state";
/**
* Returns information about the specified task.
*
* @param id
* the URN of a ViPR task
* @brief Show Task
* @return The specified task details
*/
@GET
@Path("/{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public TaskResourceRep getTask(@PathParam("id") URI id) {
Task task = queryResource(id);
// Permission Check
if (task.getTenant().equals(TenantOrg.SYSTEM_TENANT)) {
verifySystemAdmin();
} else {
verifyUserHasAccessToTenants(Lists.newArrayList(task.getTenant()));
}
return TaskMapper.toTask(task);
}
/**
* Retrieve resource representations based on input ids.
*
* @prereq none
*
* @param param
* POST data containing the id list.
*
* @brief List data of volume resources
* @return list of representations.
*/
@POST
@Path("/bulk")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Override
public TaskBulkRep getBulkResources(BulkIdParam param) {
return (TaskBulkRep) super.getBulkResources(param);
}
/**
* Returns task status count information for the specified Tenant.
*
* @brief Task Status count
* @param tenantId
* Tenant URI of the tenant the count is required for. If not supplied, the logged in users tenant will
* be used.
* A value of 'system' will return system tasks
* @return Count of tasks in different statuses
*/
@GET
@Path("/stats")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public TaskStatsRestRep getStats(@QueryParam(TENANT_QUERY_PARAM) URI tenantId) {
Set<URI> tenantIds = getTenantsFromRequest(tenantId);
verifyUserHasAccessToTenants(tenantIds);
int ready = 0;
int error = 0;
int pending = 0;
for (URI normalizedTenantId : tenantIds) {
Constraint constraint = AggregatedConstraint.Factory.getAggregationConstraint(Task.class, "tenant",
normalizedTenantId.toString(), "taskStatus");
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(Task.Status.ready.name())) {
ready++;
} else if (entry.getValue().equals(Task.Status.error.name())) {
error++;
} else {
pending++;
}
}
}
return new TaskStatsRestRep(pending, ready, error);
}
/**
* Returns a list of tasks for the specified tenant
*
* @brief Return a list of tasks for a tenant
* @param tenantId
* Tenant URI of the tenant the count is required for. If not supplied, the logged in users tenant will
* be used.
* A value of 'system' will provide a list of all the system tasks
* @return A list of tasks for the tenant
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public TasksList getTasks(@QueryParam(TENANT_QUERY_PARAM) URI tenantId,
@QueryParam(START_TIME) String startTime,
@QueryParam(END_TIME) String endTime,
@QueryParam(MAX_COUNT_PARAM) Integer max_count) {
Set<URI> tenantIds = getTenantsFromRequest(tenantId);
verifyUserHasAccessToTenants(tenantIds);
// Entries from the index, sorted with most recent first
Set<TimestampedURIQueryResult.TimestampedURI> sortedIndexEntries = Sets
.newTreeSet(new Comparator<TimestampedURIQueryResult.TimestampedURI>() {
public int compare(TimestampedURIQueryResult.TimestampedURI obj1, TimestampedURIQueryResult.TimestampedURI obj2) {
if (Objects.equals(obj1.getTimestamp(), obj2.getTimestamp())) {
return 1; // If timestampe are equal don't return 0 or TreeSet will remove one of them
} else {
return obj2.getTimestamp().compareTo(obj1.getTimestamp());
}
}
});
Date startWindowDate = TimeUtils.getDateTimestamp(startTime);
Date endWindowDate = TimeUtils.getDateTimestamp(endTime);
// Fetch index entries and load into sorted set
List<NamedRelatedResourceRep> resourceReps = Lists.newArrayList();
for (URI normalizedTenantId : tenantIds) {
TimestampedURIQueryResult taskIds = new TimestampedURIQueryResult();
_dbClient.queryByConstraint(
ContainmentConstraint.Factory.getTimedTenantOrgTaskConstraint(normalizedTenantId, startWindowDate, endWindowDate),
taskIds);
Iterator<TimestampedURIQueryResult.TimestampedURI> it = taskIds.iterator();
while (it.hasNext()) {
TimestampedURIQueryResult.TimestampedURI timestampedURI = it.next();
sortedIndexEntries.add(timestampedURI);
}
}
if (max_count == null || max_count < 0) {
max_count = FETCH_ALL;
} else {
max_count = Math.min(max_count, sortedIndexEntries.size());
}
// Produce the requested number of results
Iterator<TimestampedURIQueryResult.TimestampedURI> it = sortedIndexEntries.iterator();
int pos = 0;
while (it.hasNext() && (max_count == FETCH_ALL || pos < max_count)) {
TimestampedURIQueryResult.TimestampedURI uri = it.next();
RestLinkRep link = new RestLinkRep("self", RestLinkFactory.newLink(ResourceTypeEnum.TASK, uri.getUri()));
resourceReps.add(new NamedRelatedResourceRep(uri.getUri(), link, uri.getName()));
pos++;
}
return new TasksList(resourceReps);
}
/**
* Deletes the specified task. After this operation has been called, the task will no longer be accessible.
*
* @brief Deletes a task
* @param taskId
* ID of the task to be deleted
*/
@POST
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{taskId}/delete")
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN }, acls = { ACL.OWN })
public Response deleteTask(@PathParam("taskId") URI taskId) {
Task task = queryResource(taskId);
// Permission Check
if (task.getTenant().equals(TenantOrg.SYSTEM_TENANT)) {
verifySystemAdmin();
} else {
verifyUserHasAccessToTenants(Lists.newArrayList(task.getTenant()));
}
_dbClient.removeObject(task);
auditOp(OperationTypeEnum.DELETE_TASK, true, null, task.getId().toString(), task.getLabel());
return Response.ok().build();
}
/**
* Resumes a task. This can only be performed on a Task that has status of: suspended_no_error
* Retries a task. This can only be performed on a Task that has status of: suspended_error
*
* In the case of retry, we will retry the controller workflow starting at the failed step.
*
* @brief Resumes a task
* @param taskId
* ID of the task to be resumed
*/
@POST
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{taskId}/resume")
@CheckPermission(roles = { Role.TENANT_ADMIN, Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN }, acls = { ACL.OWN, ACL.ALL })
public Response resumeTask(@PathParam("taskId") URI taskId) {
Task task = queryResource(taskId);
// Permission Check
if (task.getTenant().equals(TenantOrg.SYSTEM_TENANT)) {
verifySystemAdmin();
} else {
verifyUserHasAccessToTenants(Lists.newArrayList(task.getTenant()));
}
Workflow workflow = validateWorkflow(task);
String opId = UUID.randomUUID().toString();
// Resume the workflow
WorkflowService.initTaskStatus(_dbClient, workflow, opId, Operation.Status.pending,
ResourceOperationTypeEnum.WORKFLOW_RESUME);
getWorkflowController().resumeWorkflow(workflow.getId(), opId);
return Response.ok().build();
}
/**
* Rolls back a task. This can only be performed on a Task with status: suspended_error
*
* @brief rolls back a task
* @param taskId
* ID of the task to roll back
*/
@POST
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{taskId}/rollback")
@CheckPermission(roles = { Role.TENANT_ADMIN, Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN }, acls = { ACL.OWN, ACL.ALL })
public Response rollbackTask(@PathParam("taskId") URI taskId) {
Task task = queryResource(taskId);
// Permission Check
if (task.getTenant().equals(TenantOrg.SYSTEM_TENANT)) {
verifySystemAdmin();
} else {
verifyUserHasAccessToTenants(Lists.newArrayList(task.getTenant()));
}
Workflow workflow = validateWorkflow(task);
String opId = UUID.randomUUID().toString();
// Rollback the workflow
WorkflowService.initTaskStatus(_dbClient, workflow, opId, Operation.Status.pending,
ResourceOperationTypeEnum.WORKFLOW_ROLLBACK);
getWorkflowController().rollbackWorkflow(workflow.getId(), opId);
return Response.ok().build();
}
private WorkflowController getWorkflowController() {
return getController(WorkflowController.class, WorkflowController.WORKFLOW_CONTROLLER_DEVICE);
}
/**
* Validate a task's workflow information for the purpose of restarting the workflow.
*
* @param task
* task object
* @return a workflow (as a convenience)
*/
private Workflow validateWorkflow(Task task) {
// Validate there is a workflow ID
if (task.getWorkflow() == null) {
throw APIException.badRequests.noWorkflowAssociatedWithTask(task.getId());
}
// Validate the workflow exists
Workflow workflow = _dbClient.queryObject(Workflow.class, task.getWorkflow());
if (workflow == null) {
throw APIException.badRequests.noWorkflowAssociatedWithURI(task.getWorkflow());
}
// Validate the workflow is in any state
if (workflow.getCompletionState() == null) {
throw APIException.badRequests.workflowCompletionStateNotFound(workflow.getId());
}
// Validate the workflow is in the right state
WorkflowState state = WorkflowState.valueOf(WorkflowState.class, workflow.getCompletionState());
EnumSet<WorkflowState> expected = EnumSet.of(WorkflowState.SUSPENDED_NO_ERROR, WorkflowState.SUSPENDED_ERROR);
ArgValidator.checkFieldForValueFromEnum(state, "Workflow completion state", expected);
return workflow;
}
/**
* @brief Assign tags to resource
* Assign tags
*
* @prereq none
*
* @param id
* the URN of a ViPR resource
* @param assignment
* tag assignments
* @return No data returned in response body
*/
@PUT
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{id}/tags")
@Override
public Tags assignTags(@PathParam("id") URI id, TagAssignment assignment) {
Task task = queryResource(id);
verifyUserHasAccessToTenants(Collections.singletonList(task.getTenant()));
return super.assignTags(id, assignment);
}
/**
* @brief List tags assigned to resource
* Returns assigned tags
*
* @prereq none
*
* @param id
* the URN of a ViPR Resource
* @return Tags information
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/{id}/tags")
@Override
public Tags getTags(@PathParam("id") URI id) {
Task task = queryResource(id);
verifyUserHasAccessToTenants(Collections.singletonList(task.getTenant()));
return super.getTags(id);
}
protected SearchResults getOtherSearchResults(Map<String, List<String>> parameters, boolean authorized) {
SearchResults searchResults = new SearchResults();
Set<URI> tenantIds = getTenantIdsFromParams(parameters);
// Resource Query
if (parameters.containsKey(RESOURCE_QUERY_PARAM)) {
URI resourceId = URI.create(parameters.get(RESOURCE_QUERY_PARAM).get(0));
List<NamedURI> tasks = TaskUtils.findResourceTaskIds(_dbClient, resourceId);
if (!tasks.isEmpty()) {
// All the tasks will have the same TenantID as the Resource
Task task = queryResource(tasks.get(0).getURI());
verifyUserHasAccessToTenants(Collections.singletonList(task.getTenant()));
}
searchResults.getResource().addAll(toSearchResults(tasks));
} else if (parameters.containsKey(STATE_PARAM)) {
// Search by task state
String state = getStringParam(STATE_PARAM, parameters);
if (state != null) {
for (URI tenant : tenantIds) {
TaskUtils.ObjectQueryResult<Task> taskResult = TaskUtils.findTenantTasks(_dbClient, tenant);
while (taskResult.hasNext()) {
Task task = taskResult.next();
if (task.getStatus().equals(state)) {
searchResults.getResource().add(toSearchResult(task.getId()));
}
}
}
}
}
return searchResults;
}
protected Task queryResource(URI id) {
ArgValidator.checkUri(id);
Task task = _dbClient.queryObject(Task.class, id);
ArgValidator.checkEntityNotNull(task, id, isIdEmbeddedInURL(id));
return task;
}
@Override
protected ResourceTypeEnum getResourceType() {
return ResourceTypeEnum.TASK;
}
@Override
public Class<Task> getResourceClass() {
return Task.class;
}
@Override
protected URI getTenantOwner(URI id) {
Task task = queryResource(id);
return task.getTenant();
}
@Override
protected ResRepFilter<? extends RelatedResourceRep> getPermissionFilter(StorageOSUser user,
PermissionsHelper permissionsHelper) {
return new TaskResRepFilter(user, permissionsHelper);
}
private Set<URI> getTenantIdsFromParams(Map<String, List<String>> parameters) {
if (!parameters.containsKey(TENANT_QUERY_PARAM) ||
parameters.get(TENANT_QUERY_PARAM).isEmpty()) {
return getTenantsFromRequest(null);
}
return getTenantsFromRequest(URI.create(parameters.get(TENANT_QUERY_PARAM).get(0)));
}
private String getStringParam(String name, Map<String, List<String>> parameters) {
List<String> values = parameters.get(name);
if (!values.isEmpty()) {
return values.get(0);
} else {
return null;
}
}
/**
* Processes the tenant id that the user provided and returns a list of tenant IDs that match the request
*/
private Set<URI> getTenantsFromRequest(URI requestedTenantId) {
Set<URI> tenants = Sets.newHashSet();
if (requestedTenantId == null) {
// No tenant specified, so return users home tenant and subtenants
tenants.add(URI.create(getUserFromContext().getTenantId()));
Map<String, Collection<String>> subTenantRoles = _permissionsHelper.getSubtenantRolesForUser(getUserFromContext());
for (Map.Entry<String, Collection<String>> subTenant : subTenantRoles.entrySet()) {
if (hasTenantAdmin(subTenant.getValue())) {
tenants.add(URI.create(subTenant.getKey()));
}
}
} else if (Objects.equals(requestedTenantId, SYSTEM_TENANT) || Objects.equals(requestedTenantId, TenantOrg.SYSTEM_TENANT)) {
tenants.add(TenantOrg.SYSTEM_TENANT);
} else {
tenants.add(requestedTenantId);
}
return tenants;
}
/**
* Verifies that the user has permission to access all the tenants in the tenants collection
*/
private void verifyUserHasAccessToTenants(Collection<URI> tenants) {
StorageOSUser user = getUserFromContext();
if (_permissionsHelper.userHasGivenRole(user, URI.create(user.getTenantId()), Role.SECURITY_ADMIN, Role.RESTRICTED_SECURITY_ADMIN,
Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN)) {
return;
}
Set<String> subtenants = _permissionsHelper.getSubtenantRolesForUser(user).keySet();
for (URI tenantId : tenants) {
if (tenantId.equals(TenantOrg.SYSTEM_TENANT)) {
verifySystemAdmin();
} else if (!tenantId.toString().equals(user.getTenantId()) &&
!subtenants.contains(tenantId.toString())) {
throw APIException.forbidden
.insufficientPermissionsForUser(user.getName());
}
}
}
private boolean hasTenantAdmin(Collection<String> roles) {
for (String role : roles) {
if (role.equals(Role.TENANT_ADMIN.name())) {
return true;
}
}
return false;
}
private List<SearchResultResourceRep> toSearchResults(List<NamedURI> items) {
List<SearchResultResourceRep> results = Lists.newArrayList();
for (NamedURI item : items) {
results.add(toSearchResult(item.getURI()));
}
return results;
}
private SearchResultResourceRep toSearchResult(URI uri) {
RestLinkRep selfLink = new RestLinkRep("self", RestLinkFactory.newLink(getResourceType(), uri));
return new SearchResultResourceRep(uri, selfLink, null);
}
/**
* Retrieve task representations based on input ids.
*
* @return list of task representations.
*/
@Override
public TaskBulkRep queryBulkResourceReps(List<URI> ids) {
Iterator<Task> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids);
return new TaskBulkRep(BulkList.wrapping(_dbIterator, MapTask.getInstance()));
}
@Override
public String getServiceType() {
return "Task";
}
@Override
protected BulkRestRep queryFilteredBulkResourceReps(List<URI> ids) {
Iterator<Task> _dbIterator = _dbClient.queryIterativeObjects(getResourceClass(), ids);
BulkList.TaskFilter filter = new BulkList.TaskFilter(getUserFromContext(), _permissionsHelper);
return new TaskBulkRep(BulkList.wrapping(_dbIterator, MapTask.getInstance(), filter));
}
public static class TaskResRepFilter<E extends RelatedResourceRep> extends ResRepFilter<E> {
public TaskResRepFilter(StorageOSUser user, PermissionsHelper permissionsHelper) {
super(user, permissionsHelper);
}
@Override
public boolean isAccessible(E resourceRep) {
Task task = _permissionsHelper.getObjectById(resourceRep.getId(), Task.class);
if (task == null) {
return false;
}
if (task.getTenant().toString().equals(_user.getTenantId())) {
return true;
}
return isTenantAccessible(task.getTenant());
}
}
}