/*
* Copyright (c) 2013 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.api.service.impl.resource;
import static com.emc.storageos.api.mapper.TaskMapper.toTask;
import static com.emc.storageos.api.mapper.WorkflowMapper.map;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static java.util.Collections.disjoint;
import javax.ws.rs.GET;
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.db.client.DbClient;
import com.emc.storageos.db.client.constraint.AlternateIdConstraint;
import com.emc.storageos.db.client.constraint.ContainmentConstraint;
import com.emc.storageos.db.client.constraint.URIQueryResultList;
import com.emc.storageos.db.client.model.OpStatusMap;
import com.emc.storageos.db.client.model.Operation;
import com.emc.storageos.db.client.model.Task;
import com.emc.storageos.db.client.model.Workflow;
import com.emc.storageos.db.client.model.WorkflowStep;
import com.emc.storageos.db.client.util.CustomQueryUtility;
import com.emc.storageos.db.client.util.NullColumnValueGetter;
import com.emc.storageos.model.ResourceOperationTypeEnum;
import com.emc.storageos.model.ResourceTypeEnum;
import com.emc.storageos.model.TaskResourceRep;
import com.emc.storageos.model.workflow.StepList;
import com.emc.storageos.model.workflow.WorkflowList;
import com.emc.storageos.model.workflow.WorkflowRestRep;
import com.emc.storageos.model.workflow.WorkflowStepRestRep;
import com.emc.storageos.security.authentication.StorageOSUser;
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.emc.storageos.svcs.errorhandling.resources.InternalException;
import com.emc.storageos.svcs.errorhandling.resources.InternalServerErrorException;
import com.emc.storageos.svcs.errorhandling.resources.InternalServerErrorExceptions;
import com.emc.storageos.workflow.WorkflowController;
import com.emc.storageos.workflow.WorkflowState;
/**
* API interface for a Workflow and WorkflowStep.
* This interface is read-only and returns historical information about
* Workflow execution.
*
* @author Watson
*/
@Path("/vdc/workflows")
@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 WorkflowService extends TaskResourceService {
protected Workflow queryResource(URI id) {
ArgValidator.checkUri(id);
Workflow workflow = _dbClient.queryObject(Workflow.class, id);
ArgValidator.checkEntityNotNull(workflow, id, isIdEmbeddedInURL(id));
return workflow;
}
/**
* Returns a list of all Workflows.
*
* @brief List all workflows
* @return WorkflowList
*/
@GET
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowList getWorkflows() {
WorkflowList list = new WorkflowList();
List<URI> workflowIds = _dbClient.queryByType(Workflow.class, true);
Iterator<Workflow> workflowIter = _dbClient.queryIterativeObjects(Workflow.class, workflowIds);
while (workflowIter.hasNext()) {
// A user that has one of the system roles can see any workflow.
// Otherwise, the workflow must have the same tenant as the user.
Workflow workflow = workflowIter.next();
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
continue;
}
}
list.getWorkflows().add(map(workflow));
}
return list;
}
/**
* Returns the active workflows.
*
* @brief List active workflows
*/
@GET
@Path("/active")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowList getActiveWorkflows() {
WorkflowList list = new WorkflowList();
List<URI> workflowIds = _dbClient.queryByType(Workflow.class, true);
Iterator<Workflow> workflowIter = _dbClient.queryIterativeObjects(Workflow.class, workflowIds);
while (workflowIter.hasNext()) {
// A user that has one of the system roles can see any workflow.
// Otherwise, the workflow must have the same tenant as the user.
Workflow workflow = workflowIter.next();
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
continue;
}
}
if (workflow.getCompleted() == false) {
list.getWorkflows().add(map(workflow));
}
}
return list;
}
/**
* Returns the completed workflows.
*
* @brief List completed workflows
*/
@GET
@Path("/completed")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowList getCompletedWorkflows() {
WorkflowList list = new WorkflowList();
List<URI> workflowIds = _dbClient.queryByType(Workflow.class, true);
Iterator<Workflow> workflowIter = _dbClient.queryIterativeObjects(Workflow.class, workflowIds);
while (workflowIter.hasNext()) {
// A user that has one of the system roles can see any workflow.
// Otherwise, the workflow must have the same tenant as the user.
Workflow workflow = workflowIter.next();
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
continue;
}
}
if (workflow.getCompleted() == true) {
list.getWorkflows().add(map(workflow));
}
}
return list;
}
/**
* Returns workflows created in the last specified number of minutes.
*
* @brief List workflows created in specified time period
*/
@GET
@Path("/recent")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowList getRecentWorkflows(@QueryParam("min") String minutes) {
if (minutes == null) {
minutes = "10";
}
Long timeDiff = new Long(minutes) * 1000 * 60;
Long currentTime = System.currentTimeMillis();
WorkflowList list = new WorkflowList();
List<URI> workflowIds = _dbClient.queryByType(Workflow.class, true);
Iterator<Workflow> workflowIter = _dbClient.queryIterativeObjects(Workflow.class, workflowIds);
while (workflowIter.hasNext()) {
// A user that has one of the system roles can see any workflow.
// Otherwise, the workflow must have the same tenant as the user.
Workflow workflow = workflowIter.next();
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
continue;
}
}
if ((currentTime - workflow.getCreationTime().getTimeInMillis()) < timeDiff) {
list.getWorkflows().add(map(workflow));
}
}
return list;
}
/**
* Returns information about the specified workflow.
*
* @param id the URN of a ViPR workflow
* @brief Show workflow
* @return Information of specific workflow
*/
@GET
@Path("/{id}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowRestRep getWorkflow(@PathParam("id") URI id) {
ArgValidator.checkFieldUriType(id, Workflow.class, "id");
Workflow workflow = queryResource(id);
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflow();
}
}
return map(workflow);
}
/**
* Gets a list of all the steps in a particular workflow.
*
* @param id the URN of a ViPR workflow
* @brief List workflow steps
* @return List of steps of a workflow
*/
@GET
@Path("/{id}/steps")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public StepList getStepList(@PathParam("id") URI id) {
ArgValidator.checkFieldUriType(id, Workflow.class, "id");
Workflow workflow = queryResource(id);
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return steps for workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflow();
}
}
StepList list = new StepList();
URIQueryResultList stepURIs = new URIQueryResultList();
_dbClient.queryByConstraint(ContainmentConstraint.Factory.getWorkflowWorkflowStepConstraint(id), stepURIs);
Iterator<URI> iter = stepURIs.iterator();
while (iter.hasNext()) {
URI workflowStepURI = iter.next();
WorkflowStep step = _dbClient.queryObject(WorkflowStep.class, workflowStepURI);
list.getSteps().add(map(step, getChildWorkflows(step)));
}
return list;
}
/**
* Returns a single WorkflowStep.
*
* @param stepId
* @brief Show workflow step
* @return Single WorkflowStep
*/
@GET
@Path("/steps/{stepid}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public WorkflowStepRestRep getStep(@PathParam("stepid") URI stepId) {
ArgValidator.checkFieldUriType(stepId, WorkflowStep.class, "stepid");
WorkflowStep step = _dbClient.queryObject(WorkflowStep.class, stepId);
ArgValidator.checkEntityNotNull(step, stepId, isIdEmbeddedInURL(stepId));
Workflow workflow = queryResource(step.getWorkflowId());
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only return workflow steps for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflowStep();
}
}
return map(step, getChildWorkflows(step));
}
/**
* Rolls back a suspended workflow.
* @preq none
* @brief Rolls back a suspended workflow
* @param uri - URI of the suspended workflow.
* @return - No data returned in response body
*/
@PUT
@Path("/{id}/rollback")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public TaskResourceRep rollbackWorkflow(@PathParam("id") URI uri) {
Workflow workflow = queryResource(uri);
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only allow rollback on workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflow();
}
}
verifySuspendedWorkflow(workflow);
String taskId = UUID.randomUUID().toString();
Operation op = initTaskStatus(_dbClient, workflow, taskId, Operation.Status.pending, ResourceOperationTypeEnum.WORKFLOW_ROLLBACK);
getController().rollbackWorkflow(uri, taskId);
return toTask(workflow,taskId, op);
}
/**
* Resumes a suspended workflow.
* @preq none
* @brief Resumes a suspended workflow
* @param uri - URI of the suspended workflow.
* @return - No data returned in response body
*/
@PUT
@Path("/{id}/resume")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public TaskResourceRep resumeWorkflow(@PathParam("id") URI uri) {
Workflow workflow = queryResource(uri);
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only allow resume on workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflow();
}
}
verifySuspendedWorkflow(workflow);
String taskId = UUID.randomUUID().toString();
Operation op = initTaskStatus(_dbClient, workflow, taskId, Operation.Status.pending, ResourceOperationTypeEnum.WORKFLOW_RESUME);
getController().resumeWorkflow(uri, taskId);
return toTask(workflow, taskId, op);
}
protected static void verifySuspendedWorkflow(Workflow workflow) {
if (workflow.getCompletionState() == null) {
throw APIException.badRequests.workflowCompletionStateNotFound(workflow.getId());
}
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);
}
/**
* Suspends a workflow as soon as possible, which is when the next step completes and all
* executing steps have completed. It is not possible to suspend in the middle of a step.
* @param uri
* @return
*/
@PUT
@Path("/{id}/suspend")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public Response suspendWorkflow(@PathParam("id") URI uri) {
return suspendWorkflowStep(uri, NullColumnValueGetter.getNullURI());
}
/**
* Suspends a workflow when it tries to execute a given step, and all other executing steps
* have suspended.
* @preq none
* @brief Suspends a workflow
* @param uri - URI of the workflow.
* @param stepURI - URI of the workflow step
* @return - No data returned in response body
*/
@PUT
@Path("/{id}/suspend/{stepId}")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@CheckPermission(roles = { Role.SYSTEM_ADMIN, Role.RESTRICTED_SYSTEM_ADMIN,
Role.SYSTEM_MONITOR, Role.TENANT_ADMIN })
public Response suspendWorkflowStep(@PathParam("id") URI uri, @PathParam("stepId") URI stepURI) {
Workflow workflow = queryResource(uri);
if (userIsOnlyTenantAdmin()) {
// User is only tenant admin so only allow rollback on workflows for that tenant.
if (!isTopLevelWorkflowForUserTenant(getTopLevelWorkflow(workflow))) {
throw APIException.badRequests.userNotAuthorizedForWorkflow();
}
}
// Verify the workflow is either RUNNING or ROLLING_BACK
EnumSet<WorkflowState> expected =
EnumSet.of(WorkflowState.RUNNING, WorkflowState.ROLLING_BACK);
if (workflow.getCompletionState() == null) {
throw APIException.badRequests.workflowCompletionStateNotFound(workflow.getId());
}
WorkflowState completionState = WorkflowState.valueOf(workflow.getCompletionState());
ArgValidator.checkFieldForValueFromEnum(completionState, "Workflow State", expected);
if (!NullColumnValueGetter.isNullURI(stepURI)) {
// Validate step id.
WorkflowStep step = _dbClient.queryObject(WorkflowStep.class, stepURI);
ArgValidator.checkEntityNotNull(step, stepURI, isIdEmbeddedInURL(stepURI));
}
String taskId = UUID.randomUUID().toString();
getController().suspendWorkflowStep(uri, stepURI, taskId);
return Response.ok().build();
}
private List<URI> getChildWorkflows(WorkflowStep step) {
URIQueryResultList result = new URIQueryResultList();
_dbClient.queryByConstraint(AlternateIdConstraint.Factory
.getWorkflowByOrchTaskId(step.getStepId()), result);
List<URI> childWorkflows = new ArrayList<URI>();
while (result.iterator().hasNext()) {
childWorkflows.add(result.iterator().next());
}
return childWorkflows;
}
private WorkflowController getController() {
return getController(WorkflowController.class, WorkflowController.WORKFLOW_CONTROLLER_DEVICE);
}
/**
* Convenience method for initializing a task object with a status
*
* @param workflow export group
* @param task task ID
* @param status status to initialize with
* @param opType operation type
* @return operation object
*/
protected static Operation initTaskStatus(DbClient dbClient, Workflow workflow, String task, Operation.Status status, ResourceOperationTypeEnum opType) {
if (workflow.getOpStatus() == null) {
workflow.setOpStatus(new OpStatusMap());
}
Operation op = new Operation();
op.setResourceType(opType);
if (status == Operation.Status.ready) {
op.ready();
}
dbClient.createTaskOpStatus(Workflow.class, workflow.getId(), task, op);
return op;
}
@Override
protected URI getTenantOwner(URI id) {
return null;
}
@Override
protected ResourceTypeEnum getResourceType() {
return ResourceTypeEnum.WORKFLOW;
}
/**
* Determines if the user only has tenant admin roles, which is the case
* if it does not have any of the system level roles allowed by the service.
*
* @return true if user had tenant admin role, but none of the supported system roles, otherwise false.
*/
private boolean userIsOnlyTenantAdmin() {
return !userHasRoles(Role.SYSTEM_ADMIN.name(), Role.RESTRICTED_SYSTEM_ADMIN.name(), Role.SYSTEM_MONITOR.name());
}
/**
* Determines if the user has one of the passed roles.
*
* @param roles The roles to verify
*
* @return true if the user has one of the passed roles, else false
*/
private boolean userHasRoles(String... roles) {
StorageOSUser user = getUserFromContext();
Set<String> userRoles = user.getRoles();
return !disjoint(userRoles, Arrays.asList(roles));
}
/**
* Gets the top-level workflow for the passed workflow. Returns the passed workflow
* if it has no parent workflow and is therefore a top-level workflow.
*
* @param workflow A reference to the workflow
*
* @return
*/
private Workflow getTopLevelWorkflow(Workflow workflow) {
Workflow topLevelWorkflow = workflow;
Workflow parentWorkflow = getParentWorkflow(workflow);
if (parentWorkflow != null) {
topLevelWorkflow = getTopLevelWorkflow(parentWorkflow);
}
return topLevelWorkflow;
}
/**
* Gets the parent workflow for the passed workflow.
*
* @param workflow A reference to the workflow
*
* @return The parent workflow or null for a top-level workflow.
*/
private Workflow getParentWorkflow(Workflow workflow) {
Workflow parentWorkflow = null;
if (workflow != null) {
List<WorkflowStep> wfSteps = CustomQueryUtility.queryActiveResourcesByConstraint(
_dbClient, WorkflowStep.class, AlternateIdConstraint.Factory.getWorkflowStepByStepId(workflow.getOrchTaskId()));
if (!wfSteps.isEmpty()) {
// There should only be a single workflow step that has a step id that is equal
// to the orchestration task id for a workflow. The workflow for that step is
// the parent workflow for the passed workflow.
URI parentWorkflowURI = wfSteps.get(0).getWorkflowId();
parentWorkflow = queryResource(parentWorkflowURI);
}
}
return parentWorkflow;
}
/**
* Determines if the passed top-level workflow is valid for the user's tenant.
* Child workflows should not be passed to this routine.
*
* @param topLevelWorkflow A reference to a top-level workflow.
*
* @return true if the user's tenant is the workflow tenant.
*/
private boolean isTopLevelWorkflowForUserTenant(Workflow topLevelWorkflow) {
boolean workflowForTenant = false;
// Since the tenant is not directly associated to a workflow, we find the
// Task instance(s) whose request id is the same as the orchestration task
// id for the workflow. We can then get the tenant from the task and compare
// that tenant to the user tenant.
String wfOrchTaskId = topLevelWorkflow.getOrchTaskId();
List<Task> wfTasks = CustomQueryUtility.queryActiveResourcesByConstraint(
_dbClient, Task.class, AlternateIdConstraint.Factory.getTasksByRequestIdConstraint(wfOrchTaskId));
if (!wfTasks.isEmpty()) {
for (Task wfTask : wfTasks) {
// There could actually be multiple tasks. For example, when creating a VPLEX
// local volume, there will be a Task created for the VPLEX volume and another
// Task for the backend volume and both will have the same request id. Additionally,
// the tenant for backend volume Task will always be the root tenant rather than
// tenant of the user creating the volume. Only the VPLEX volume Task will have that
// tenant. So, if the tenant of any of the Task instances found with the workflow's
// orchestration task id has the current user's tenant, then the workflow is for
// the tenant.
if (wfTask.getTenant().toString().equals(getUserFromContext().getTenantId())) {
workflowForTenant = true;
break;
}
}
} else {
throw APIException.internalServerErrors.cannotFindTaskForWorkflow(
topLevelWorkflow.getLabel(), topLevelWorkflow.getId().toString(), wfOrchTaskId);
}
return workflowForTenant;
}
}