package org.ovirt.engine.core.bll; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.ejb.TransactionRolledbackLocalException; import javax.inject.Inject; import javax.transaction.Status; import javax.transaction.SystemException; import javax.transaction.Transaction; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.bll.aaa.SessionDataContainer; import org.ovirt.engine.core.bll.aaa.SsoSessionUtils; import org.ovirt.engine.core.bll.context.CommandContext; import org.ovirt.engine.core.bll.context.CompensationContext; import org.ovirt.engine.core.bll.context.DefaultCompensationContext; import org.ovirt.engine.core.bll.context.EngineContext; import org.ovirt.engine.core.bll.context.NoOpCompensationContext; import org.ovirt.engine.core.bll.interfaces.BackendInternal; import org.ovirt.engine.core.bll.job.ExecutionContext; import org.ovirt.engine.core.bll.job.ExecutionHandler; import org.ovirt.engine.core.bll.quota.InvalidQuotaParametersException; import org.ovirt.engine.core.bll.quota.QuotaConsumptionParameter; import org.ovirt.engine.core.bll.quota.QuotaConsumptionParametersWrapper; import org.ovirt.engine.core.bll.quota.QuotaManager; import org.ovirt.engine.core.bll.quota.QuotaStorageDependent; import org.ovirt.engine.core.bll.quota.QuotaVdsDependent; import org.ovirt.engine.core.bll.tasks.CommandCoordinatorUtil; import org.ovirt.engine.core.bll.tasks.interfaces.Command; import org.ovirt.engine.core.bll.tasks.interfaces.CommandCallback; import org.ovirt.engine.core.bll.tasks.interfaces.SPMTask; import org.ovirt.engine.core.bll.utils.PermissionSubject; import org.ovirt.engine.core.common.AuditLogType; import org.ovirt.engine.core.common.VdcObjectType; import org.ovirt.engine.core.common.action.LockProperties; import org.ovirt.engine.core.common.action.LockProperties.Scope; import org.ovirt.engine.core.common.action.VdcActionParametersBase; import org.ovirt.engine.core.common.action.VdcActionParametersBase.CommandExecutionReason; import org.ovirt.engine.core.common.action.VdcActionParametersBase.EndProcedure; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.asynctasks.AsyncTaskCreationInfo; import org.ovirt.engine.core.common.asynctasks.AsyncTaskType; import org.ovirt.engine.core.common.businessentities.ActionGroup; import org.ovirt.engine.core.common.businessentities.AsyncTask; import org.ovirt.engine.core.common.businessentities.BusinessEntity; import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot; import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot.EntityStatusSnapshot; import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot.SnapshotType; import org.ovirt.engine.core.common.businessentities.CommandEntity; import org.ovirt.engine.core.common.businessentities.IVdsAsyncCommand; import org.ovirt.engine.core.common.businessentities.QuotaEnforcementTypeEnum; import org.ovirt.engine.core.common.businessentities.SubjectEntity; import org.ovirt.engine.core.common.businessentities.TransientCompensationBusinessEntity; import org.ovirt.engine.core.common.businessentities.aaa.DbUser; import org.ovirt.engine.core.common.errors.EngineError; import org.ovirt.engine.core.common.errors.EngineException; import org.ovirt.engine.core.common.errors.EngineFault; import org.ovirt.engine.core.common.errors.EngineMessage; import org.ovirt.engine.core.common.interfaces.VDSBrokerFrontend; import org.ovirt.engine.core.common.job.JobExecutionStatus; import org.ovirt.engine.core.common.job.Step; import org.ovirt.engine.core.common.job.StepEnum; import org.ovirt.engine.core.common.job.StepSubjectEntity; import org.ovirt.engine.core.common.queries.VdcQueryParametersBase; import org.ovirt.engine.core.common.queries.VdcQueryReturnValue; import org.ovirt.engine.core.common.queries.VdcQueryType; import org.ovirt.engine.core.common.utils.ExecutionMethod; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.common.utils.PersistedCommandContext; import org.ovirt.engine.core.common.utils.ValidationUtils; import org.ovirt.engine.core.common.vdscommands.VDSCommandType; import org.ovirt.engine.core.common.vdscommands.VDSParametersBase; import org.ovirt.engine.core.common.vdscommands.VDSReturnValue; import org.ovirt.engine.core.compat.CommandStatus; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.TransactionScopeOption; import org.ovirt.engine.core.dal.dbbroker.DbFacade; import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogDirector; import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogable; import org.ovirt.engine.core.dal.dbbroker.auditloghandling.AuditLogableBase; import org.ovirt.engine.core.dal.job.ExecutionMessageDirector; import org.ovirt.engine.core.dao.BusinessEntitySnapshotDao; import org.ovirt.engine.core.dao.EntityDao; import org.ovirt.engine.core.dao.GenericDao; import org.ovirt.engine.core.dao.PermissionDao; import org.ovirt.engine.core.dao.StatusAwareDao; import org.ovirt.engine.core.dao.StepDao; import org.ovirt.engine.core.utils.CorrelationIdTracker; import org.ovirt.engine.core.utils.Deserializer; import org.ovirt.engine.core.utils.ReflectionUtils; import org.ovirt.engine.core.utils.ReplacementUtils; import org.ovirt.engine.core.utils.SerializationFactory; import org.ovirt.engine.core.utils.lock.EngineLock; import org.ovirt.engine.core.utils.lock.LockManager; import org.ovirt.engine.core.utils.transaction.NoOpTransactionCompletionListener; import org.ovirt.engine.core.utils.transaction.TransactionCompletionListener; import org.ovirt.engine.core.utils.transaction.TransactionMethod; import org.ovirt.engine.core.utils.transaction.TransactionSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import com.woorea.openstack.base.client.OpenStackResponseException; public abstract class CommandBase<T extends VdcActionParametersBase> extends AuditLogableBase implements TransactionMethod<Object>, Command<T> { protected static final String SYSTEM_USER_NAME = "SYSTEM"; private static final String DEFAULT_TASK_KEY = "DEFAULT_TASK_KEY"; private T parameters; private VdcReturnValueBase returnValue; private CommandActionState actionState = CommandActionState.EXECUTE; private VdcActionType actionType; private final List<Class<?>> validationGroups = new ArrayList<>(); private final Guid commandId; private boolean quotaChanged = false; private String description = ""; private TransactionScopeOption scope; private TransactionScopeOption endActionScope; private List<QuotaConsumptionParameter> consumptionParameters; protected Map<String, Serializable> commandData; private Long sessionSeqId; @Inject protected AuditLogDirector auditLogDirector; @Inject protected LockManager lockManager; @Inject private QuotaManager quotaManager; @Inject private SessionDataContainer sessionDataContainer; @Inject private BackendInternal backendInternal; @Inject private VDSBrokerFrontend vdsBroker; @Inject private ObjectCompensation objectCompensation; @Inject protected ExecutionHandler executionHandler; @Inject private EntityDao entityDao; @Inject private BusinessEntitySnapshotDao businessEntitySnapshotDao; @Inject private PermissionDao permissionDao; @Inject private StepDao stepDao; /** Indicates whether the acquired locks should be released after the execute method or not */ private boolean releaseLocksAtEndOfExecute = true; protected Logger log = LoggerFactory.getLogger(getClass()); /** The context defines how to monitor the command and handle its compensation */ private final CommandContext context; /** A map contains the properties for describing the job */ protected Map<String, String> jobProperties; private CommandStatus commandStatus = CommandStatus.NOT_STARTED; protected CommandBase(T parameters, CommandContext cmdContext) { this.context = cmdContext; this.commandData = new HashMap<>(); this.parameters = parameters; Guid commandIdFromParameters = parameters.getCommandId(); if (commandIdFromParameters == null) { commandIdFromParameters = Guid.newGuid(); getParameters().setCommandId(commandIdFromParameters); } commandId = commandIdFromParameters; } /** * Constructor for command creation when compensation is applied on startup */ protected CommandBase(Guid commandId) { this.context = new CommandContext(new EngineContext()); this.commandId = commandId; this.commandData = new HashMap<>(); } /** * @see PostConstruct */ @PostConstruct protected final void postConstruct() { if (!isCompensationContext()) { initCommandBase(); init(); } else { this.context.withCompensationContext(createDefaultCompensationContext()); } } protected void initUser() { DbUser user = getSessionDataContainer().getUser(context.getEngineContext().getSessionId(), true); if (user != null) { setCurrentUser(user); } if (getSessionDataContainer().getPrincipalName(context.getEngineContext().getSessionId()) == null) { // command was most probably executed from Quartz job, so session doesn't contain any user info // we need to set username to fake internal user so audit logs will not contain "null@N/A" as username setUserName(SYSTEM_USER_NAME); } else { setUserName(getSessionDataContainer().getUserName(context.getEngineContext().getSessionId())); } } private boolean isCompensationContext() { return getParameters() == null; } /** * Initializes CommandBase instance when parameters are passed in constructor (non compensation * context instance creation) */ private void initCommandBase() { initUser(); ExecutionContext executionContext = context.getExecutionContext(); if (executionContext.getJob() != null) { setJobId(executionContext.getJob().getId()); } else if (executionContext.getStep() != null) { setJobId(executionContext.getStep().getJobId()); } setCorrelationId(parameters.getCorrelationId()); } /** * Implement this method whenever you need extra initialization of the command after the * constructor. All DB calls or other interaction with the command dependencies for initialization * should be done here. It is ensured that all injected dependencies were injected at the time calling. */ protected void init() { } /** * Checks if possible to perform rollback using command, and if so performs it * * @param commandType * command type for the rollback * @param params * parameters for the rollback * @param context * context for the rollback * @return result of the command execution */ protected VdcReturnValueBase attemptRollback(VdcActionType commandType, VdcActionParametersBase params, CommandContext context) { if (canPerformRollbackUsingCommand(commandType, params)) { params.setExecutionReason(CommandExecutionReason.ROLLBACK_FLOW); params.setTransactionScopeOption(TransactionScopeOption.RequiresNew); return getBackend().runInternalAction(commandType, params, context); } return new VdcReturnValueBase(); } protected BackendInternal getBackend() { return backendInternal; } /** * Checks if possible to perform rollback using command, and if so performs it * * @param commandType * command type for the rollback * @param params * parameters for the rollback * @param context * context for the rollback * @return result of the command execution */ protected VdcReturnValueBase checkAndPerformRollbackUsingCommand(VdcActionType commandType, VdcActionParametersBase params, CommandContext context) { return attemptRollback(commandType, params, context); } /** * Checks if it is possible to rollback the command using a command (and not VDSM) * * @param commandType * the rollback command to be executed * @param params * parameters for the rollback command * @return true if it is possible to run rollback using command */ protected boolean canPerformRollbackUsingCommand (VdcActionType commandType, VdcActionParametersBase params) { return true; } /** * Create an appropriate compensation context. The default is one that does nothing for command that don't run in a * transaction, and a real one for commands that run in a transaction. * * @param transactionScopeOption * The transaction scope. * @return The compensation context to use. */ private CompensationContext createCompensationContext(TransactionScopeOption transactionScopeOption, boolean forceCompensation) { if (transactionScopeOption == TransactionScopeOption.Suppress && !forceCompensation) { return NoOpCompensationContext.getInstance(); } return createDefaultCompensationContext(); } protected DefaultCompensationContext createDefaultCompensationContext() { DefaultCompensationContext defaultContext = new DefaultCompensationContext(); defaultContext.setCommandId(commandId); defaultContext.setCommandType(getClass().getName()); defaultContext.setBusinessEntitySnapshotDao(businessEntitySnapshotDao); defaultContext.setSnapshotSerializer( SerializationFactory.getSerializer()); return defaultContext; } /** * @return the compensationContext */ public CompensationContext getCompensationContext() { return context.getCompensationContext(); } /** * @param compensationContext the compensationContext to set */ public void setCompensationContext(CompensationContext compensationContext) { context.withCompensationContext(compensationContext); } /** * Validates that the pre-conditions for command execution are met. * This method is called internally from the code. * <p> * In general, each command has its own conditions which should met, in order to expect a valid command execution. * An attempt to execute a command which failed to meet all the condition will lead to unpredicted result and should * be avoided. * <p> * The violated condition messages are stored by {@link #addValidationMessage(EngineMessage)} and can be reviewed * in {@link VdcReturnValueBase#getValidationMessages()} retrieved by {@link #getReturnValue()} * * @return VdcReturnValueBase A container object for the operation result. */ public VdcReturnValueBase validateOnly() { setActionMessageParameters(); getReturnValue().setValid(internalValidate()); String tempVar = getDescription(); getReturnValue().setDescription((tempVar != null) ? tempVar : getReturnValue().getDescription()); return returnValue; } public VdcReturnValueBase executeAction() { getSessionDataContainer().updateSessionLastActiveTime(getParameters().getSessionId()); determineExecutionReason(); actionState = CommandActionState.EXECUTE; String tempVar = getDescription(); getReturnValue().setDescription((tempVar != null) ? tempVar : getReturnValue().getDescription()); setActionMessageParameters(); Step validatingStep=null; boolean actionAllowed; boolean isExternal = this.getParameters().getJobId() != null || this.getParameters().getStepId() != null; if (!isExternal) { validatingStep = executionHandler.addStep(getExecutionContext(), StepEnum.VALIDATING, null); } try { if (parentHasCallback()) { persistCommand(getParameters().getParentCommand()); } actionAllowed = getReturnValue().isValid() || internalValidate(); if (!isExternal) { executionHandler.endStep(getExecutionContext(), validatingStep, actionAllowed); } if (actionAllowed) { execute(); } else { getReturnValue().setValid(false); } } finally { updateCommandIfNeeded(); freeLockExecute(); clearAsyncTasksWithOutVdsmId(); } return getReturnValue(); } private void clearAsyncTasksWithOutVdsmId() { if (!getReturnValue().getTaskPlaceHolderIdList().isEmpty()) { TransactionSupport.executeInNewTransaction(() -> { for (Guid asyncTaskId : getReturnValue().getTaskPlaceHolderIdList()) { AsyncTask task = CommandCoordinatorUtil.getAsyncTaskFromDb(asyncTaskId); if (task != null && Guid.isNullOrEmpty(task.getVdsmTaskId())) { CommandCoordinatorUtil.removeTaskFromDbByTaskId(task.getTaskId()); } } return null; }); } } private void determineExecutionReason() { if (getParameters().getExecutionReason() == null) { getParameters().setExecutionReason(CommandExecutionReason.REGULAR_FLOW); } } /** * Run the default compensation logic (inside a new transaction):<br> * <ol> * <li>Get all the entity snapshots that this command has created.</li> * <li>For each snapshot:</li> * <ol> * <li>Deserialize the entity.</li> * <li>Using the entity Dao:</li> * <ul> * <li>If the entity was added by the command, remove it.</li> * <li>Otherwise, If the entity is not in DB anymore, restore it.</li> * <li>Otherwise, update it.</li> * </ul> * </ol> * <li>Remove all the snapshots for this command, since we handled them.</li> </ol> */ @SuppressWarnings({ "unchecked", "synthetic-access" }) protected void compensate() { try { if (isQuotaDependant()) { rollbackQuota(); } } catch (NullPointerException e) { log.debug("RollbackQuota: failed (may be because quota is disabled)", e); } // If the compensation data is not for the command do not perform compensation. if (!commandId.equals(getCompensationContext().getCommandId())) { return; } TransactionSupport.executeInNewTransaction(() -> { Deserializer deserializer = SerializationFactory.getDeserializer(); List<BusinessEntitySnapshot> entitySnapshots = businessEntitySnapshotDao.getAllForCommandId(commandId); log.debug("Command [id={}]: {} compensation data.", commandId, entitySnapshots.isEmpty() ? "No" : "Going over"); for (BusinessEntitySnapshot snapshot : entitySnapshots) { Class<Serializable> snapshotClass = (Class<Serializable>) ReflectionUtils.getClassFor(snapshot.getSnapshotClass()); Serializable snapshotData = deserializer.deserialize(snapshot.getEntitySnapshot(), snapshotClass); log.info("Command [id={}]: Compensating {} of {}; snapshot: {}.", commandId, snapshot.getSnapshotType(), snapshot.getEntityType(), snapshot.getSnapshotType() == SnapshotType.DELETED_OR_UPDATED_ENTITY ? "id=" + snapshot.getEntityId() : snapshotData.toString()); Class<BusinessEntity<Serializable>> entityClass = (Class<BusinessEntity<Serializable>>) ReflectionUtils.getClassFor(snapshot.getEntityType()); switch (snapshot.getSnapshotType()) { case CHANGED_STATUS_ONLY: EntityStatusSnapshot entityStatusSnapshot = (EntityStatusSnapshot) snapshotData; ((StatusAwareDao<Serializable, Enum<?>>) getDaoForEntity(entityClass)).updateStatus( entityStatusSnapshot.getId(), entityStatusSnapshot.getStatus()); break; case DELETED_OR_UPDATED_ENTITY: deletedOrUpdateEntity(entityClass, (BusinessEntity<Serializable>) snapshotData); break; case UPDATED_ONLY_ENTITY: getDaoForEntity(entityClass).update((BusinessEntity<Serializable>)snapshotData); break; case NEW_ENTITY_ID: getDaoForEntity(entityClass).remove(snapshotData); break; case TRANSIENT_ENTITY: objectCompensation.compensate(CommandBase.this, (TransientCompensationBusinessEntity) snapshotData); break; default: throw new IllegalArgumentException(String.format( "Unknown %s value, unable to compensate value %s.", SnapshotType.class.getName(), snapshot.getSnapshotType())); } } getCompensationContext().afterCompensationCleanup(); return null; }); } /* * method introduced to fix variable scope in switches (as nested scope is not created for switches), * when static analysis disallows nested blocks */ private void deletedOrUpdateEntity(Class<BusinessEntity<Serializable>> entityClass, BusinessEntity<Serializable> entitySnapshot) { GenericDao<BusinessEntity<Serializable>, Serializable> daoForEntity = getDaoForEntity(entityClass); if (daoForEntity.get(entitySnapshot.getId()) == null) { daoForEntity.save(entitySnapshot); } else { daoForEntity.update(entitySnapshot); } } private GenericDao<BusinessEntity<Serializable>, Serializable> getDaoForEntity( Class<BusinessEntity<Serializable>> entityClass) { return DbFacade.getInstance().getDaoForEntity(entityClass); } protected void handleStepsOnEnd() { if (getCommandStep() != null && getExecutionContext().getStep() != null) { executionHandler.endTaskStep(getExecutionContext().getStep().getId(), isEndSuccessfully() ? JobExecutionStatus.FINISHED : JobExecutionStatus.FAILED); } if (getCommandStep() == null) { executionHandler.startFinalizingStep(getExecutionContext()); } } /** * Returns whether the command updates the progress on its added {@link Step}. * @return boolean */ public boolean shouldUpdateStepProgress() { return false; } @Override public VdcReturnValueBase endAction() { boolean shouldEndAction = handleCommandExecutionEnded(); if (shouldEndAction) { handleStepsOnEnd(); handleChildCommands(); try { initiateLockEndAction(); setActionState(); handleTransactivity(); TransactionSupport.executeInScope(endActionScope, this); } catch (TransactionRolledbackLocalException e) { log.info("endAction: Transaction was aborted in {}", this.getClass().getName()); } finally { endStepsAndJobIfNeeded(); freeLockEndAction(); // NOTE: this update persists updates made during the endSuccessfully()/endWithFailure() execution. // The update is done intentionally after the freeLock() call, change with care. updateCommandIfNeeded(); if (getCommandShouldBeLogged()) { logCommand(); } } } else { getReturnValue().setSucceeded(true); } return getReturnValue(); } private void endStepsAndJobIfNeeded() { //TODO: getEndActionTryAgain() isn't supported currently by the coco infrastructure boolean endActionWillRunAgain = !getSucceeded() && getReturnValue().getEndActionTryAgain(); if (endActionWillRunAgain) { return; } boolean succeeded = getSucceeded() && getParameters().getTaskGroupSuccess(); executionHandler.endFinalizingStepAndCurrentStep(getContext().getExecutionContext(), succeeded); if (!parentHasCallback()) { executionHandler.endTaskJobIfNeeded(getContext().getExecutionContext(), getSucceeded() && getParameters() .getTaskGroupSuccess()); } } /** * The following method should initiate a lock , in order to release it at endAction() */ private void initiateLockEndAction() { if (context.getLock() == null) { LockProperties lockProperties = getLockProperties(); if (Scope.Command.equals(lockProperties.getScope())) { context.withLock(buildLock()); } } } private void handleTransactivity() { scope = (getParameters() != null) ? getParameters().getTransactionScopeOption() : TransactionScopeOption.Required; endActionScope = scope; boolean forceCompensation = getForceCompensation(); // @NonTransactiveAttribute annotation overrides the scope passed by the // command parameters if (!getTransactive()) { scope = TransactionScopeOption.Suppress; // Set the end action scope to suppress only for non-compensating commands, or the end action for commands // will run without transaction but compensation is not supported for end action. endActionScope = forceCompensation ? endActionScope : scope; } if (getCompensationContext() == null) { context.withCompensationContext(createCompensationContext(scope, forceCompensation)); } } private void setActionState() { // This mechanism should change, // And for ROLLBACK_FLOW we should // introduce a new actionState. // Currently it was decided that ROLLBACK_FLOW will cause endWithFailure if (isEndSuccessfully()) { actionState = CommandActionState.END_SUCCESS; } else { actionState = CommandActionState.END_FAILURE; } } public void handleChildCommands() { if (getCallback() != null) { List<Guid> childCommands = CommandCoordinatorUtil.getChildCommandIds(getCommandId()); List<VdcActionParametersBase> parameters = new LinkedList<>(); for (Guid id : childCommands) { CommandBase<?> command = CommandCoordinatorUtil.retrieveCommand(id); if (command.getParameters().getEndProcedure() == EndProcedure.PARENT_MANAGED || command.getParameters().getEndProcedure() == EndProcedure.FLOW_MANAGED) { command.getParameters().setEndProcedure(EndProcedure.FLOW_MANAGED); command.getParameters().setCommandType(command.getActionType()); parameters.add(command.getParameters()); } } getParameters().setImagesParameters(parameters); } } protected boolean isEndSuccessfully() { return getParameters().getTaskGroupSuccess() && getParameters().getExecutionReason() == CommandExecutionReason.REGULAR_FLOW; } private boolean isEndProcedureApplicableToEndAction() { return getParameters().getEndProcedure() == EndProcedure.FLOW_MANAGED || getParameters().getEndProcedure() == EndProcedure.COMMAND_MANAGED; } private boolean handleCommandExecutionEnded() { boolean shouldEndAction = parentHasCallback() ? isEndProcedureApplicableToEndAction() : true; CommandStatus newStatus = isEndSuccessfully() ? CommandStatus.SUCCEEDED : CommandStatus.FAILED; if (getCallback() == null) { setCommandStatus(newStatus); if (!shouldEndAction) { logEndWillBeExecutedByParent(newStatus); } } return shouldEndAction; } public void logEndWillBeExecutedByParent(CommandStatus status) { log.info("Command [id={}]: Updating status to '{}', The command end method logic will be executed by one of its parent commands.", getCommandId(), status); } public void endActionInTransactionScope() { boolean exceptionOccurred = false; try { if (isEndSuccessfully()) { internalEndSuccessfully(); setCommandStatus(CommandStatus.ENDED_SUCCESSFULLY, false); } else { internalEndWithFailure(); setCommandStatus(CommandStatus.ENDED_WITH_FAILURE, false); } } catch (RuntimeException e) { exceptionOccurred = true; throw e; } finally { if (TransactionSupport.current() == null) { try { getCompensationContext().cleanupCompensationDataAfterSuccessfulCommand(); } catch (RuntimeException e) { logExceptionAndCompensate(e); } } else { try { if (!exceptionOccurred && TransactionSupport.current().getStatus() == Status.STATUS_ACTIVE) { getCompensationContext().cleanupCompensationDataAfterSuccessfulCommand(); } else { compensate(); } } catch (SystemException e) { logExceptionAndCompensate(e); } } } } /** * Log the exception & call compensate. * * @param e * The exception to log. */ protected void logExceptionAndCompensate(Exception e) { log.error("Exception while wrapping-up compensation in endAction", e); compensate(); } private void internalEndSuccessfully() { log.info("Ending command '{}' successfully.", getClass().getName()); endSuccessfully(); } protected void endSuccessfully() { setSucceeded(true); } void logRenamedEntity() { if (this instanceof RenamedEntityInfoProvider) { RenamedEntityInfoProvider renameable = (RenamedEntityInfoProvider) this; String oldEntityName = renameable.getEntityOldName(); String newEntityName = renameable.getEntityNewName(); if (!StringUtils.equals(oldEntityName, newEntityName)) { // log entity rename details AuditLogableBase logable = new AuditLogableBase(); String entityType = renameable.getEntityType(); logable.addCustomValue("EntityType", entityType); logable.addCustomValue("OldEntityName", oldEntityName); logable.addCustomValue("NewEntityName", newEntityName); logable.addCustomValue("UserName", getCurrentUser().getLoginName()); renameable.setEntityId(logable); auditLog(logable, AuditLogType.ENTITY_RENAMED); } } } protected void auditLog(AuditLogable logable, AuditLogType logType) { auditLogDirector.log(logable, logType); } private void internalEndWithFailure() { log.error("Ending command '{}' with failure.", getClass().getName()); endWithFailure(); rollbackQuota(); } private void rollbackQuota() { // Quota accounting is done only in the most external Command. if (isQuotaChanged()) { List<QuotaConsumptionParameter> consumptionParameters = getQuotaConsumptionParameters(); if (consumptionParameters != null) { for (QuotaConsumptionParameter parameter : consumptionParameters) { getQuotaManager().removeQuotaFromCache(getStoragePool().getId(), parameter.getQuotaGuid()); } } } } protected List<QuotaConsumptionParameter> getQuotaConsumptionParameters() { // This a double marking mechanism which was created to ensure Quota dependencies would not be inherited // by descendants commands. Each Command is both marked by the QuotaDependency and implements the required // Interfaces (NONE does not implement any of the two interfaces). // The enum markings prevent Quota dependencies unintentional inheritance. if (consumptionParameters == null) { switch (getActionType().getQuotaDependency()) { case NONE: return null; case STORAGE: consumptionParameters = getThisQuotaStorageDependent().getQuotaStorageConsumptionParameters(); break; case CLUSTER: consumptionParameters = getThisQuotaVdsDependent().getQuotaVdsConsumptionParameters(); break; default: consumptionParameters = getThisQuotaStorageDependent().getQuotaStorageConsumptionParameters(); consumptionParameters.addAll(getThisQuotaVdsDependent().getQuotaVdsConsumptionParameters()); break; } } return consumptionParameters; } private QuotaStorageDependent getThisQuotaStorageDependent() { return (QuotaStorageDependent) this; } private QuotaVdsDependent getThisQuotaVdsDependent() { return (QuotaVdsDependent) this; } protected void endWithFailure() { setSucceeded(true); rollbackQuota(); } private boolean isValidateSupportsTransaction() { return getClass().isAnnotationPresent(ValidateSupportsTransaction.class); } private boolean internalValidate() { boolean returnValue = false; try { Transaction transaction = null; if (!isValidateSupportsTransaction()) { transaction = TransactionSupport.suspend(); } try { returnValue = isUserAuthorizedToRunAction() && validateInputs() && acquireLock() && validate() && internalValidateAndSetQuota(); if (!returnValue && getReturnValue().getValidationMessages().size() > 0) { log.warn("Validation of action '{}' failed for user {}. Reasons: {}", getActionType(), getUserName(), StringUtils.join(getReturnValue().getValidationMessages(), ',')); } } finally { if (transaction != null) { TransactionSupport.resume(transaction); } } } catch (DataAccessException dataAccessEx) { log.error("Data access error during ValidateFailure.", dataAccessEx); addValidationMessage(EngineMessage.CAN_DO_ACTION_DATABASE_CONNECTION_FAILURE); } catch (RuntimeException ex) { log.error("Error during ValidateFailure.", ex); addValidationMessage(EngineMessage.CAN_DO_ACTION_GENERAL_FAILURE); } finally { if (!returnValue) { setCommandStatus(CommandStatus.ENDED_WITH_FAILURE); freeLock(); } } return returnValue; } private boolean internalValidateAndSetQuota() { // Quota accounting is done only in the most external Command. if (!isQuotaDependant()) { return true; } QuotaConsumptionParametersWrapper quotaConsumptionParametersWrapper = new QuotaConsumptionParametersWrapper(this, getReturnValue().getValidationMessages()); quotaConsumptionParametersWrapper.setParameters(getQuotaConsumptionParameters()); List<QuotaConsumptionParameter> quotaParams = quotaConsumptionParametersWrapper.getParameters(); if (quotaParams == null) { throw new InvalidQuotaParametersException("Command: " + this.getClass().getName() + ". No Quota parameters available."); } // Some commands are not quotable, given the values of their parameters. // e.g AddDisk is storage-quotable but when the disk type is external LUN there is no storage pool to it. // scenarios like this must set its QuotaConsumptionParameter to an empty list. if (quotaParams.isEmpty()) { return true; } if (getStoragePool() == null) { throw new InvalidQuotaParametersException("Command: " + this.getClass().getName() + ". Storage pool is not available for quota calculation. "); } boolean result = getQuotaManager().consume(quotaConsumptionParametersWrapper); setQuotaChanged(result); return result; } protected boolean isQuotaDependant() { if (getActionType().getQuotaDependency() == VdcActionType.QuotaDependency.NONE) { return false; } if (!isInternalExecution()) { return true; } return getActionType().isQuotaDependentAsInternalCommand(); } /** * @return true if all parameters class and its inner members passed * validation */ protected boolean validateInputs() { return validateObject(getParameters()); } protected boolean validateObject(Object value) { List<String> messages = ValidationUtils.validateInputs(getValidationGroups(), value); if (!messages.isEmpty()) { getReturnValue().getValidationMessages().addAll(messages); return false; } return true; } /** * Set the parameters for bll messages (such as type and action). * The parameters should be initialized through the command that is called, * instead set them at the validate() */ protected void setActionMessageParameters() { // No-op method for inheritors to implement } protected List<Class<?>> getValidationGroups() { return validationGroups; } protected List<Class<?>> addValidationGroup(Class<?>... validationGroup) { validationGroups.addAll(Arrays.asList(validationGroup)); return validationGroups; } /** * Checks if the current user is authorized to run the given action on the given object. * * @param userId * user id to check * @param object * the object to check * @param type * the type of the object to check * @return <code>true</code> if the current user is authorized to run the action, <code>false</code> otherwise */ protected boolean checkUserAuthorization(Guid userId, final ActionGroup actionGroup, final Guid object, final VdcObjectType type) { // Grant if there is matching permission in the database: final Guid permId = permissionDao.getEntityPermissions(userId, actionGroup, object, type); if (permId != null) { if (log.isDebugEnabled()) { log.debug("Found permission '{}' for user when running '{}', on '{}' with id '{}'", permId, getActionType(), type.getVdcObjectTranslation(), object); } return true; } // Deny otherwise: if (log.isDebugEnabled()) { log.debug("No permission found for user when running action '{}', on object '{}' for action group '{}' with id '{}'.", getActionType(), type.getVdcObjectTranslation(), actionGroup, object); } return false; } /** * Checks if the input user and groups is authorized to run the given action on the given object. * * @param userId * the user to check * @param groupIds * the groups to check * @param actionGroup * the action group to check * @param object * the object to check * @param type * the type of the object to check * @param ignoreEveryone * if true, the "everyone" will not be considered * @return <code>true</code> if the current user is authorized to run the action, <code>false</code> otherwise */ protected boolean checkUserAndGroupsAuthorization(Guid userId, Collection<Guid> groupIds, final ActionGroup actionGroup, final Guid object, final VdcObjectType type, final boolean ignoreEveryone) { // Grant if there is matching permission in the database: if (log.isDebugEnabled()) { log.debug("Checking whether user '{}' or groups '{}' have action group '{}' on object type '{}'", userId, groupIds, actionGroup, object, type.name()); } final Guid permId = permissionDao.getEntityPermissionsForUserAndGroups(userId, StringUtils.join(groupIds, ","), actionGroup, object, type, ignoreEveryone); if (permId != null) { if (log.isDebugEnabled()) { log.debug("Found permission '{}' for user when running '{}', on '{}' with id '{}'", permId, getActionType(), type.getVdcObjectTranslation(), object); } return true; } // Deny otherwise: if (log.isDebugEnabled()) { log.debug("No permission found for user when running action '{}', on object '{}' for action group '{}' with id '{}'.", getActionType(), type.getVdcObjectTranslation(), actionGroup, object); } return false; } /** * Check if current user is authorized to run current action. Skip check if * MLA is off or command is internal. * * @return <code>true</code> if the user is authorized to run the given action, * <code>false</code> otherwise */ protected boolean isUserAuthorizedToRunAction() { // Skip check if this is an internal action: if (isInternalExecution()) { if (log.isDebugEnabled()) { log.debug("Permission check skipped for internal action {}.", getActionType()); } return true; } // Skip check if multilevel administration is disabled: if (!MultiLevelAdministrationHandler.isMultilevelAdministrationOn()) { if (log.isDebugEnabled()) { log.debug("Permission check for action '{}' skipped because multilevel administration is disabled.", getActionType()); } return true; } // Deny the permissions if there is no logged in user: if (getCurrentUser() == null) { addValidationMessage(EngineMessage.USER_IS_NOT_LOGGED_IN); return false; } // Get identifiers and types of the objects whose permissions have to be // checked: final List<PermissionSubject> permSubjects = getPermissionCheckSubjects(); if (permSubjects == null || permSubjects.isEmpty()) { if (log.isDebugEnabled()) { log.debug("The set of objects to check is null or empty for action '{}'.", getActionType()); } addValidationMessage(EngineMessage.USER_NOT_AUTHORIZED_TO_PERFORM_ACTION); return false; } if (isQuotaDependant()) { addQuotaPermissionSubject(permSubjects); } if (log.isDebugEnabled()) { StringBuilder builder = getPermissionSubjectsAsStringBuilder(permSubjects); log.debug("Checking whether user '{}' or one of the groups he is member of, have the following permissions: {}", getCurrentUser().getId(), builder.toString()); } // If we are here then we should grant the permission: return checkPermissions(permSubjects); } protected boolean checkPermissions(final List<PermissionSubject> permSubjects) { for (PermissionSubject permSubject : permSubjects) { if (!checkSinglePermission(permSubject, getReturnValue().getValidationMessages())) { logMissingPermission(permSubject); return false; } } return true; } protected void logMissingPermission(PermissionSubject permSubject) { log.info("No permission found for user '{}' or one of the groups he is member of," + " when running action '{}', Required permissions are: Action type: '{}' Action group: '{}'" + " Object type: '{}' Object ID: '{}'.", getCurrentUser().getId(), getActionType(), permSubject.getActionGroup().getRoleType().name(), permSubject.getActionGroup().name(), permSubject.getObjectType().getVdcObjectTranslation(), permSubject.getObjectId()); } public final boolean checkSinglePermission(PermissionSubject permSubject, Collection<String> messages) { final Guid objectId = permSubject.getObjectId(); final VdcObjectType objectType = permSubject.getObjectType(); final ActionGroup objectActionGroup = permSubject.getActionGroup(); // if objectId is null we can't check permission if (objectId == null) { if (log.isDebugEnabled()) { log.debug("The object to check is null for action '{}'.", getActionType()); } messages.add(EngineMessage.USER_NOT_AUTHORIZED_TO_PERFORM_ACTION.name()); return false; } // Check that an action group is defined for this action; if (objectActionGroup == null) { if (log.isDebugEnabled()) { log.debug("No action group is defined for action '{}'.", getActionType()); } return false; } // Check the authorization: if (!checkUserAuthorization(getCurrentUser().getId(), objectActionGroup, objectId, objectType)) { messages.add(permSubject.getMessage().name()); return false; } return true; } public void addQuotaPermissionSubject(List<PermissionSubject> quotaPermissionList) { // if quota enforcement is not in HARD_ENFORCEMENT the quota may be null. if (!isInternalExecution() && getStoragePool() != null && getStoragePool().getQuotaEnforcementType() != QuotaEnforcementTypeEnum.DISABLED && getStoragePool().getQuotaEnforcementType() != QuotaEnforcementTypeEnum.SOFT_ENFORCEMENT) { List<QuotaConsumptionParameter> consumptionParameters = getQuotaConsumptionParameters(); if (consumptionParameters != null) { consumptionParameters.stream() .filter(parameter -> parameter.getQuotaGuid() != null) .filter(parameter -> !Guid.Empty.equals(parameter.getQuotaGuid())) .filter(parameter -> QuotaConsumptionParameter.QuotaAction.RELEASE != parameter.getQuotaAction()) .map(parameter -> new PermissionSubject(parameter.getQuotaGuid(), VdcObjectType.Quota, ActionGroup.CONSUME_QUOTA, EngineMessage.USER_NOT_AUTHORIZED_TO_CONSUME_QUOTA)) .forEach(quotaPermissionList::add); } } } /** * Validates that the pre-conditions for command execution are met. * <p> * In general, each command has its own conditions which should met, in order to expect a valid command execution. * An attempt to execute a command which failed to meet all the condition will lead to unpredicted result and should * be avoided. * <p> * The violated condition messages are stored by {@link #addValidationMessage(EngineMessage)} and can be reviewed * in {@link VdcReturnValueBase#getValidationMessages()} retrieved by {@link #getReturnValue()} * * @return {@code true} if the command can be executed, else {@code false} * * @see ValidateSupportsTransaction */ protected boolean validate() { return true; } /** * Factory to determine the type of the ReturnValue field */ protected VdcReturnValueBase createReturnValue() { return new VdcReturnValueBase(); } protected boolean getSucceeded() { return getReturnValue().getSucceeded(); } protected void setSucceeded(boolean value) { getReturnValue().setSucceeded(value); } public boolean getCommandShouldBeLogged() { return getParameters().getShouldBeLogged(); } public void setCommandShouldBeLogged(boolean value) { getParameters().setShouldBeLogged(value); } protected void setActionReturnValue(Object value) { getReturnValue().setActionReturnValue(value); } protected Object getActionReturnValue() { return getReturnValue().getActionReturnValue(); } protected boolean isExecutedAsChildCommand() { return getParameters().getParentCommand() != VdcActionType.Unknown; } /** * Calculates the proper parameters for the task * @param parentCommandType parent command type for which the task is created * @param parameters parameter of the creating command */ protected VdcActionParametersBase getParametersForTask(VdcActionType parentCommandType, VdcActionParametersBase parameters) { // If there is no parent command, the command that its type // will be stored in the DB for thr task is the one creating the command VdcActionParametersBase parentParameters = parameters.getParentParameters(); if (parentCommandType == VdcActionType.Unknown || parentParameters == null) { return parameters; } // The parent parameters are the ones that are kept for the task. // In order to make sure that in case of rollback-by-command, the ROLLBACK // flow will be called, the execution reason of the child command is set // to the one of the parent command (if its REGULAR_FLOW, the execution // reason of the parent command remains REGULAR_FLOW). parentParameters.setExecutionReason(parameters.getExecutionReason()); parentParameters.setCommandType(parentCommandType); return parentParameters; } private boolean executeWithoutTransaction() { boolean functionReturnValue = false; boolean exceptionOccurred = true; try { logRunningCommand(); executeCommand(); functionReturnValue = getSucceeded(); exceptionOccurred = false; } catch (EngineException e) { log.error("Command '{}' failed: {}", getClass().getName(), e.getMessage()); log.debug("Exception", e); processExceptionToClient(new EngineFault(e, e.getVdsError().getCode())); } catch (OpenStackResponseException e) { // Adding a message to executeFailedMessages is needed only when the list is empty if (returnValue.getExecuteFailedMessages().isEmpty()) { processExceptionToClient(new EngineFault(e, EngineError.ENGINE)); } log.error("Command '{}' failed: {}", getClass().getName(), e.getMessage()); log.error("Exception", e); } catch (RuntimeException e) { processExceptionToClient(new EngineFault(e, EngineError.ENGINE)); log.error("Command '{}' failed: {}", getClass().getName(), e.getMessage()); log.error("Exception", e); } finally { if (!exceptionOccurred) { setCommandExecuted(); } // If we failed to execute due to exception or some other reason, we compensate for the failure. if (exceptionOccurred || !getSucceeded()) { setSucceeded(false); compensate(); if (commandStatus == CommandStatus.ACTIVE) { setCommandStatus(noAsyncOperations() ? CommandStatus.ENDED_WITH_FAILURE : CommandStatus.EXECUTION_FAILED); } } else { // if the command is not an async task and has no custom callback // set the status to ENDED_SUCCESSFULLY if the status is ACTIVE if (getReturnValue().getVdsmTaskIdList().isEmpty() && getReturnValue().getInternalVdsmTaskIdList().isEmpty() && getCallback() == null && commandStatus == CommandStatus.ACTIVE) { setCommandStatus(CommandStatus.ENDED_SUCCESSFULLY); } getCompensationContext().cleanupCompensationDataAfterSuccessfulCommand(); } } return functionReturnValue; } protected TransactionScopeOption getTransactionScopeOption() { return getParameters().getTransactionScopeOption(); } private String getCommandParamatersString(T params) { StringBuilder buf = new StringBuilder(); List<String> methodNames = ReflectionUtils.getGetterMethodNames(params); methodNames.removeAll(ReflectionUtils.getGetterMethodNames(new VdcActionParametersBase())); for (String methodName : methodNames) { Method method = ReflectionUtils.getLoggableMethodWithNoArgs(params, methodName); if (method == null) { continue; } Object retVal = ReflectionUtils.invokeMethodWithNoArgs(params, method); if (buf.length() > 0) { buf.append(", "); } buf.append(getFieldName(methodName)); buf.append(" = "); buf.append(retVal == null ? "null" : retVal.toString()); } return buf.toString(); } private String getFieldName(String methodName) { String GET_ROOT = "get"; String IS_ROOT = "is"; return methodName.startsWith(GET_ROOT) ? methodName.substring(GET_ROOT.length()) : methodName.substring(IS_ROOT.length()); } /** * Log the running command , and log the affected entity id and type (if * there are any). */ private void logRunningCommand() { // Set start of log for running command. StringBuilder logInfo = new StringBuilder("Running command: ") .append(getClass().getSimpleName()); if (log.isDebugEnabled()) { logInfo.append(getParameters() != null ? "(" + getCommandParamatersString(getParameters()) + ")" : StringUtils.EMPTY); } logInfo.append(" internal: ").append(isInternalExecution()).append("."); // Get permissions of object ,to get object id. List<PermissionSubject> permissionSubjectList = getPermissionCheckSubjects(); // Log if there is entry in the permission map. if (permissionSubjectList != null && !permissionSubjectList.isEmpty()) { // Build entities string for entities affected by this operation. StringBuilder logEntityIdsInfo = getPermissionSubjectsAsStringBuilder(permissionSubjectList); // If found any entities, add the log to the logInfo. if (logEntityIdsInfo.length() != 0) { // Print all the entities affected. logInfo.append(" Entities affected : ").append( logEntityIdsInfo); } } // Log the final appended message to the log. log.info("{}", logInfo); } private StringBuilder getPermissionSubjectsAsStringBuilder(List<PermissionSubject> permissionSubjects) { StringBuilder builder = new StringBuilder(); // Iterate all over the entities , which should be affected. permissionSubjects.stream().filter(permSubject -> permSubject.getObjectId() != null).forEach(permSubject -> { // Add comma when there are more than one entity affected. if (builder.length() != 0) { builder.append(", "); } builder.append(" ID: ").append(permSubject.getObjectId()) .append(" Type: ").append(permSubject.getObjectType()); if (permSubject.getActionGroup() != null) { builder.append("Action group ") .append(permSubject.getActionGroup().name()) .append(" with role type ") .append(permSubject.getActionGroup().getRoleType().name()); } }); return builder; } private void executeActionInTransactionScope() { registerRollbackHandler(new DefaultCommandTransactionCompletionListener()); // If we didn't managed to acquire lock for command or the object wasn't managed to execute properly, then // rollback the transaction. if (!executeWithoutTransaction()) { if (TransactionSupport.current() == null) { cancelTasks(); } // we don't want to commit transaction here TransactionSupport.setRollbackOnly(); } } protected void registerRollbackHandler(TransactionCompletionListener transactionCompletionListener) { if (TransactionSupport.current() != null) { TransactionSupport.registerRollbackHandler(transactionCompletionListener); } } protected boolean parentHasCallback() { if (isExecutedAsChildCommand() && getParameters().getParentParameters() != null) { CommandEntity commandEntity = CommandCoordinatorUtil.getCommandEntity(getParameters().getParentParameters().getCommandId()); return commandEntity != null && commandEntity.isCallbackEnabled(); } return false; } private void handleCommandStepAndEntities() { if (getCommandStep() != null) { Step taskStep = executionHandler.addTaskStep(getExecutionContext(), getCommandStep(), ExecutionMessageDirector.resolveStepMessage(getCommandStep(), getJobMessageProperties()), getCommandStepSubjectEntities()); if (taskStep != null) { if (shouldUpdateStepProgress()) { stepDao.updateStepProgress(taskStep.getId(), 0); } getExecutionContext().setStep(taskStep); persistCommandIfNeeded(); } } } protected final void execute() { setCommandStatus(CommandStatus.ACTIVE); getReturnValue().setValid(true); getReturnValue().setIsSyncronious(true); if (shouldPersistCommand()) { persistCommandIfNeeded(); CommandCoordinatorUtil.persistCommandAssociatedEntities(getCommandId(), getSubjectEntities()); } executionHandler.addStep(getExecutionContext(), StepEnum.EXECUTING, null); handleCommandStepAndEntities(); try { handleTransactivity(); TransactionSupport.executeInScope(scope, this); } catch (TransactionRolledbackLocalException e) { log.info("Transaction was aborted in '{}'", this.getClass().getName()); // Transaction was aborted - we must sure we compensation for all previous applicative stages of the command compensate(); } finally { try { if (getCommandShouldBeLogged()) { logCommand(); } if (getSucceeded()) { if (getCommandShouldBeLogged()) { logRenamedEntity(); } // only after creating all tasks, we can start polling them (we // don't want // to start polling before all tasks were created, otherwise we // might change // the VM/VmTemplate status to 'Down'/'OK' too soon. startPollingAsyncTasks(); } } finally { if (noAsyncOperations() && !executionHandler.checkIfJobHasTasks(getExecutionContext())) { executionHandler.endJob(getExecutionContext(), getSucceeded()); } } } } public boolean hasTasks() { return !getReturnValue().getVdsmTaskIdList().isEmpty(); } private boolean getForceCompensation() { NonTransactiveCommandAttribute annotation = getClass().getAnnotation(NonTransactiveCommandAttribute.class); return annotation != null && annotation.forceCompensation(); } protected abstract void executeCommand(); /** * provides the information on child commands */ protected void buildChildCommandInfos() { } /** * calls execute action the child command. */ protected VdcReturnValueBase runCommand(CommandBase<?> command) { VdcReturnValueBase returnValue = command.executeAction(); returnValue.setCorrelationId(command.getParameters().getCorrelationId()); returnValue.setJobId(command.getJobId()); return returnValue; } private void logCommand() { Class<?> type = getClass(); InternalCommandAttribute annotation = type.getAnnotation(InternalCommandAttribute.class); if (annotation == null) { log(); } } protected void log() { final Transaction transaction = TransactionSupport.suspend(); try { auditLogDirector.log(this, getAuditLogTypeValue()); } catch (final RuntimeException ex) { log.error("Error during log command: {}. Exception {}", getClass().getName(), ex.getMessage()); log.debug("Exception", ex); } finally { TransactionSupport.resume(transaction); } } private boolean getTransactive() { NonTransactiveCommandAttribute annotation = getClass().getAnnotation(NonTransactiveCommandAttribute.class); return annotation == null; } public Map<String, Serializable> getCommandData() { return commandData; } public void setCommandData(Map<String, Serializable> commandData) { this.commandData = commandData; } @Override public T getParameters() { return parameters; } public VdcReturnValueBase getReturnValue() { if (returnValue == null) { returnValue = createReturnValue(); } return returnValue; } public void setReturnValue(VdcReturnValueBase returnValue) { this.returnValue = returnValue; } public VdcActionType getActionType() { try { if (actionType == null) { String name = getClass().getName(); name = name.substring(0, name.length() - 7); name = name.substring(name.lastIndexOf('.') + 1); actionType = VdcActionType.valueOf(name); } return actionType; } catch (Exception e) { return VdcActionType.Unknown; } } protected String getDescription() { return description; } protected void setDescription(String value) { description = value; } private void processExceptionToClient(EngineFault fault) { fault.setSessionID(getParameters().getSessionId()); returnValue.getExecuteFailedMessages().add(fault.getError().name()); returnValue.setFault(fault); } Map<String, Guid> taskKeyToTaskIdMap = new HashMap<>(); public Guid persistAsyncTaskPlaceHolder(VdcActionType parentCommand) { return persistAsyncTaskPlaceHolder(parentCommand, DEFAULT_TASK_KEY); } public Guid persistAsyncTaskPlaceHolder(VdcActionType parentCommand, final String taskKey) { Guid taskId = Guid.Empty; try { AsyncTaskCreationInfo creationInfo = new AsyncTaskCreationInfo(); creationInfo.setTaskType(getTaskType()); final AsyncTask task = createAsyncTask(creationInfo, parentCommand); taskId = task.getTaskId(); TransactionScopeOption scopeOption = getTransactive() ? TransactionScopeOption.RequiresNew : TransactionScopeOption.Required; TransactionSupport.executeInScope(scopeOption, () -> { saveTaskAndPutInMap(taskKey, task); return null; }); addToReturnValueTaskPlaceHolderIdList(taskId); } catch (RuntimeException ex) { log.error("Error during persistAsyncTaskPlaceHolder for command '{}': {}", getClass().getName(), ex.getMessage()); log.error("Exception", ex); } return taskId; } private void saveTaskAndPutInMap(String taskKey, AsyncTask task) { CommandCoordinatorUtil.saveAsyncTaskToDb(task); taskKeyToTaskIdMap.put(taskKey, task.getTaskId()); } private void addToReturnValueTaskPlaceHolderIdList(Guid taskId) { if (!getReturnValue().getTaskPlaceHolderIdList().contains(taskId)) { getReturnValue().getTaskPlaceHolderIdList().add(taskId); } } public void deleteAsyncTaskPlaceHolder() { deleteAsyncTaskPlaceHolder(DEFAULT_TASK_KEY); } public void deleteAsyncTaskPlaceHolder(String taskKey) { Guid taskId = taskKeyToTaskIdMap.remove(taskKey); if (!Guid.isNullOrEmpty(taskId)) { CommandCoordinatorUtil.removeTaskFromDbByTaskId(taskId); } } public Guid getAsyncTaskId() { return getAsyncTaskId(DEFAULT_TASK_KEY); } public Guid getAsyncTaskId(String taskKey) { if (!taskKeyToTaskIdMap.containsKey(taskKey)) { return Guid.Empty; } return taskKeyToTaskIdMap.get(taskKey); } /** * Use this method in order to create task in the CommandCoordinatorUtil in a safe way. If you use this method within a * certain command, make sure that the command implemented the ConcreteCreateTask method. * * @param taskId * id of task to create * @param asyncTaskCreationInfo * info to send to CommandCoordinatorUtil when creating the task. * @param parentCommand * VdcActionType of the command that its endAction we want to invoke when tasks are finished. * @param entitiesMap * maps ID of entity to its type. * @return Guid of the created task. */ protected Guid createTask(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, Map<Guid, VdcObjectType> entitiesMap) { return createTask(taskId, asyncTaskCreationInfo, parentCommand, null, entitiesMap); } /** * Same as {@link #createTask(Guid, AsyncTaskCreationInfo, VdcActionType, VdcObjectType, Guid...)} * but without suspending the current transaction. * * Note: it is better to use {@link #createTask(Guid, AsyncTaskCreationInfo, VdcActionType, VdcObjectType, Guid...)} * since it suspend the current transaction, thus the changes are being updated in the * DB right away. call this method only you have a good reason for it and * the current transaction is short. * * @see {@link #createTask(Guid, AsyncTaskCreationInfo, VdcActionType, VdcObjectType, Guid...)} */ protected Guid createTaskInCurrentTransaction(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, VdcObjectType entityType, Guid... entityIds) { return createTaskImpl(taskId, asyncTaskCreationInfo, parentCommand, null, entityType, entityIds); } /** * Use this method in order to create task in the CommandCoordinatorUtil in a safe way. If you use this method within a * certain command, make sure that the command implemented the ConcreteCreateTask method. * * @param taskId * if of task to create * @param asyncTaskCreationInfo * info to send to CommandCoordinatorUtil when creating the task. * @param parentCommand * VdcActionType of the command that its endAction we want to invoke when tasks are finished. * @return Guid of the created task. */ public Guid createTask(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand) { return createTask(taskId, asyncTaskCreationInfo, parentCommand, null, // The reason Collections.emptyMap is not used here as // the map should be mutable new HashMap<>()); } protected Guid createTask(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, VdcObjectType vdcObjectType, Guid... entityIds) { return createTask(taskId, asyncTaskCreationInfo, parentCommand, createEntitiesMapForSingleEntityType(vdcObjectType, entityIds)); } protected Guid createTask(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, String description, VdcObjectType entityType, Guid... entityIds) { return createTask(taskId, asyncTaskCreationInfo, parentCommand, description, createEntitiesMapForSingleEntityType(entityType, entityIds)); } /** * Use this method in order to create task in the CommandCoordinatorUtil in a safe way. If you use this method within a * certain command, make sure that the command implemented the ConcreteCreateTask method. * * @param asyncTaskCreationInfo * info to send to CommandCoordinatorUtil when creating the task. * @param parentCommand * VdcActionType of the command that its endAction we want to invoke when tasks are finished. * @param description * A message which describes the task * @param entitiesMap - map of entities */ protected Guid createTask(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, String description, Map<Guid, VdcObjectType> entitiesMap) { Transaction transaction = TransactionSupport.suspend(); try { return createTaskImpl(taskId, asyncTaskCreationInfo, parentCommand, description, entitiesMap); } catch (RuntimeException ex) { log.error("Error during createTask for command '{}': {}", getClass().getName(), ex.getMessage()); log.error("Exception", ex); } finally { TransactionSupport.resume(transaction); } return Guid.Empty; } private Guid createTaskImpl(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, String description, VdcObjectType entityType, Guid... entityIds) { return createTaskImpl(taskId, asyncTaskCreationInfo, parentCommand, description, createEntitiesMapForSingleEntityType(entityType, entityIds)); } private Map<Guid, VdcObjectType> createEntitiesMapForSingleEntityType(VdcObjectType entityType, Guid... entityIds) { Map<Guid, VdcObjectType> entitiesMap = new HashMap<>(); for (Guid entityId : entityIds) { entitiesMap.put(entityId, entityType); } return entitiesMap; } private Guid createTaskImpl(Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand, String description, Map<Guid, VdcObjectType> entitiesMap) { return CommandCoordinatorUtil.createTask(taskId, this, asyncTaskCreationInfo, parentCommand, description, entitiesMap); } /** * Create the {@link SPMTask} object to be run * @param taskId the id of the async task place holder in the database * @param asyncTaskCreationInfo Info on how to create the task * @param parentCommand The type of command issuing the task * @return An {@link SPMTask} object representing the task to be run */ public SPMTask concreteCreateTask( Guid taskId, AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand) { return CommandCoordinatorUtil.concreteCreateTask(taskId, this, asyncTaskCreationInfo, parentCommand); } public StepEnum getCommandStep() { return null; } public List<StepSubjectEntity> getCommandStepSubjectEntities() { return Collections.emptyList(); } public VdcActionParametersBase getParentParameters(VdcActionType parentCommand) { VdcActionParametersBase parentParameters = getParametersForTask(parentCommand, getParameters()); if (parentParameters.getParametersCurrentUser() == null && getCurrentUser() != null) { parentParameters.setParametersCurrentUser(getCurrentUser()); } return parentParameters; } private AsyncTask createAsyncTask( AsyncTaskCreationInfo asyncTaskCreationInfo, VdcActionType parentCommand) { return CommandCoordinatorUtil.createAsyncTask(this, asyncTaskCreationInfo, parentCommand); } /** @return The type of task that should be created for this command. * Commands that do not create async tasks return notSupported **/ protected AsyncTaskType getTaskType() { return AsyncTaskType.notSupported; } public AsyncTaskType getAsyncTaskType() { if (getTaskType() == AsyncTaskType.notSupported) { throw new UnsupportedOperationException(); } return getTaskType(); } public void startPollingAsyncTasks(Collection<Guid> taskIds) { taskIds.forEach(CommandCoordinatorUtil::startPollingTask); } protected boolean noAsyncOperations() { return !hasTasks() && getCallback() == null; } protected void startPollingAsyncTasks() { startPollingAsyncTasks(getReturnValue().getVdsmTaskIdList()); } public ArrayList<Guid> getTaskIdList() { return (isExecutedAsChildCommand() && !parentHasCallback()) ? getReturnValue().getInternalVdsmTaskIdList() : getReturnValue().getVdsmTaskIdList(); } private void cancelTasks() { CommandCoordinatorUtil.cancelTasks(this); } protected void revertTasks() { CommandCoordinatorUtil.revertTasks(this); } protected EngineLock getLock() { return context.getLock(); } protected void setLock(EngineLock lock) { context.withLock(lock); } /** * The default lock property settings for the commands */ protected final LockProperties getLockingPropertiesSettings() { return LockProperties.create(Scope.None).withWait(false); } /** * Commands that need exclusive locks will override this method * to provide custom locking property settings */ protected LockProperties applyLockProperties(LockProperties lockProperties) { return lockProperties; } /** * gets the lock properties for the command, sets the properties in the * command parameters */ protected LockProperties getLockProperties() { LockProperties lockProperties = parameters.getLockProperties(); if (lockProperties == null) { lockProperties = applyLockProperties(getLockingPropertiesSettings()); parameters.setLockProperties(lockProperties); } return lockProperties; } protected boolean acquireLock() { LockProperties lockProperties = getLockProperties(); boolean returnValue = true; if (!Scope.None.equals(lockProperties.getScope())) { releaseLocksAtEndOfExecute = Scope.Execution.equals(lockProperties.getScope()); if (!lockProperties.isWait()) { returnValue = acquireLockInternal(); } else { acquireLockAndWait(); } } return returnValue; } public boolean reacquireLocks() { return acquireLockAsyncTask(); } /** * The following method should be called after restart of engine during initialization of asynchronous task */ public final boolean acquireLockAsyncTask() { LockProperties lockProperties = getLockProperties(); boolean returnValue = true; if (!Scope.None.equals(lockProperties.getScope())) { releaseLocksAtEndOfExecute = Scope.Execution.equals(lockProperties.getScope()); if (!releaseLocksAtEndOfExecute) { returnValue = acquireLockInternal(); } } return returnValue; } protected boolean acquireLockInternal() { // if commandLock is null then we acquire new lock, otherwise probably we got lock from caller command. if (context.getLock() == null) { EngineLock lock = buildLock(); if (lock != null) { Pair<Boolean, Set<String>> lockAcquireResult = lockManager.acquireLock(lock); if (lockAcquireResult.getFirst()) { log.info("Lock Acquired to object '{}'", lock); context.withLock(lock); } else { log.info("Failed to Acquire Lock to object '{}'", lock); getReturnValue().getValidationMessages() .addAll(extractVariableDeclarations(lockAcquireResult.getSecond())); return false; } } } return true; } /** * This method gets {@link Iterable} of strings that might contain * variable declarations inside them, and return a new List in which * every variable declaration is extracted to a separate string in * order to conform the convention of the can-do-action messages. * for example: * "ACTION_TYPE_FAILED_TEMPLATE_IS_USED_FOR_CREATE_VM$VmName MyVm" * will be splited to 2 strings: * "ACTION_TYPE_FAILED_TEMPLATE_IS_USED_FOR_CREATE_VM" and "$VmName MyVm" */ protected List<String> extractVariableDeclarations(Iterable<String> appendedValidateMsgs) { final List<String> result = new ArrayList<>(); for (String appendedValidateMsg : appendedValidateMsgs) { result.addAll(Arrays.asList(appendedValidateMsg.split("(?=\\$)"))); } return result; } private EngineLock buildLock() { EngineLock lock = null; Map<String, Pair<String, String>> exclusiveLocks = getExclusiveLocks(); Map<String, Pair<String, String>> sharedLocks = getSharedLocks(); if (exclusiveLocks != null || sharedLocks != null) { lock = new EngineLock(exclusiveLocks, sharedLocks); } return lock; } private void acquireLockAndWait() { // if commandLock is null then we acquire new lock, otherwise probably we got lock from caller command. if (context.getLock() == null) { Map<String, Pair<String, String>> exclusiveLocks = getExclusiveLocks(); if (exclusiveLocks != null) { EngineLock lock = new EngineLock(exclusiveLocks, null); log.info("Before acquiring and wait lock '{}'", lock); lockManager.acquireLockWait(lock); context.withLock(lock); log.info("Lock-wait acquired to object '{}'", lock); } } } private void freeLockExecute() { if (releaseLocksAtEndOfExecute || !getSucceeded() || (noAsyncOperations() && !(this instanceof IVdsAsyncCommand))) { freeLock(); } } /** * If the command has more than one task handler, we can reach the end action * phase and in that phase execute the next task handler. In that case, we * don't want to release the locks, so we ask whether we're not in execute state. */ private void freeLockEndAction() { if (getActionState() != CommandActionState.EXECUTE) { freeLock(); } } protected void freeLock() { if (context.getLock() != null) { lockManager.releaseLock(context.getLock()); log.info("Lock freed to object '{}'", context.getLock()); context.withLock(null); // free other locks here to guarantee they will be freed only once freeCustomLocks(); } } /** hook for subclasses that hold additional custom locks */ protected void freeCustomLocks() { } /** * The following method should return a map which is represent exclusive lock * <p> * Lock structure: * <pre> * Map { * locked entity identifier (usually id, it follows selected locking group), * Pair { * locking group, * message to be shown when this lock prevents acquisition of another * lock/ other operation * } * } * </pre> * </p> * Message has form of {@link EngineMessage} constant followed by possible variable * replacements as parsed by {@link #extractVariableDeclarations} * * @see LockMessagesMatchUtil */ protected Map<String, Pair<String, String>> getExclusiveLocks() { return null; } /** * The following method should return a map which is represent shared lock * * @see #getExclusiveLocks() */ protected Map<String, Pair<String, String>> getSharedLocks() { return null; } @Override public Object runInTransaction() { if (actionState == CommandActionState.EXECUTE) { executeActionInTransactionScope(); } else { endActionInTransactionScope(); } return null; } /** * Use for call chaining of validation commands, so that their result will be validated and kept in the messages if * the validation had failed.<br> * <br> * <b>Example:</b> * * <pre> * boolean isValid = validate(SomeValidator.validateSomething(param1, param2, ...)); * </pre> * * @param validationResult * The validation result from the inline call to validate. * @return <code>true</code> if the validation was successful, and <code>false</code> if it wasn't. */ protected boolean validate(ValidationResult validationResult) { if (!validationResult.isValid()) { addValidationMessages(validationResult.getMessages()); validationResult.getVariableReplacements().forEach(this::addValidationMessage); } return validationResult.isValid(); } /** * Add a message to the {@link CommandBase#validate()}'s return value. * This return value will be sent to the client for the detailed information * of why the action can't be performed. * * @param message * The message to add. */ protected void addValidationMessage(EngineMessage message) { getReturnValue().getValidationMessages().add(message.name()); } protected void addValidationMessages(List<EngineMessage> messages) { getReturnValue().getValidationMessages().addAll(messages.stream().map(m -> m.name()).collect(Collectors.toList())); } /** * Add validation message with variable replacements and return false. * * @param message the message to add * @param variableReplacements variable replacements * @return false always * @see {@link #addValidationMessage(String)} */ protected final boolean failValidation(EngineMessage message, String ... variableReplacements) { return failValidation(message, Arrays.asList(variableReplacements)); } protected final boolean failValidation(EngineMessage message, Collection<String> variableReplacements) { return failValidation(Collections.singletonList(message), variableReplacements); } protected final boolean failValidation(List<EngineMessage> messages, String ... variableReplacements) { return failValidation(messages, Arrays.asList(variableReplacements)); } protected final boolean failValidation(List<EngineMessage> messages, Collection<String> variableReplacements) { addValidationMessages(messages); for (String variableReplacement : variableReplacements) { addValidationMessage(variableReplacement); } return false; } /** * Add a message to the {@link CommandBase#validate()}'s return value. * This return value will be sent to the client for the detailed information of why the action can't be performed. * * @param message The message to add. */ protected void addValidationMessage(String message) { getReturnValue().getValidationMessages().add(message); } /** * Add a variable to the {@link CommandBase#validate()}'s return value. * The variable will be formatted as "$varName varValue" and will be used to parse the placeholders defined * in the validate message itself * * @param varName the variable name * @param varValue the variable value */ protected void addValidationMessageVariable(String varName, Object varValue) { getReturnValue().getValidationMessages().add(ReplacementUtils.createSetVariableString(varName, varValue)); } /** * Run the given command in the VDS and return the VDS's response. * * @param commandType * The command to run. * @param parameters * The corresponding parameters for the command. * @return The return from the VDS, containing success/failure, async task ids (in case of success), or error data * (in case of failure). * @throws org.ovirt.engine.core.common.errors.EngineException * In case of an unhandled exception (Usually more severe than failure of the command, because we don't * know why). */ protected VDSReturnValue runVdsCommand(VDSCommandType commandType, VDSParametersBase parameters) throws EngineException { return getVdsBroker().runVdsCommand(commandType, parameters); } /** * Permissions are attached to object so every command must declare its * object target type and its GUID * * @return Map of GUIDs to Object types */ public abstract List<PermissionSubject> getPermissionCheckSubjects(); /** * returns a collection of the command subject entities */ protected Collection<SubjectEntity> getSubjectEntities() { return Collections.emptyList(); } /** * Returns the properties which used to populate the job message. The default properties resolving will use * {@link #getPermissionCheckSubjects()} to get the entities associated with the command. The property key is the * type of the entity by {@code VdcObjectType.name()} and the value is the name of the entity or the entity * {@code Guid} in case non-resolvable entity name. * * @return A map which contains the data to be used to populate the {@code Job} description. */ public Map<String, String> getJobMessageProperties() { jobProperties = new HashMap<>(); List<PermissionSubject> subjects = getPermissionCheckSubjects(); if (!subjects.isEmpty()) { VdcObjectType entityType; Guid entityId; String value; for (PermissionSubject permSubject : subjects) { entityType = permSubject.getObjectType(); entityId = permSubject.getObjectId(); if (entityType != null && entityId != null) { value = entityDao.getEntityNameByIdAndType(entityId, entityType); if (value == null) { value = entityId.toString(); } jobProperties.put(entityType.name().toLowerCase(), value); } } } return jobProperties; } public void setExecutionContext(ExecutionContext executionContext) { context.withExecutionContext(executionContext); } public ExecutionContext getExecutionContext() { return context.getExecutionContext(); } public Guid getCommandId() { return commandId; } public CommandContext getContext() { return context; } /** * Adds a sub step on the current execution context by providing parent and new step information and step description * @param parentStep parent step to add the new sub step on * @param newStep step to add * @param description description of step to be added */ protected Step addSubStep(StepEnum parentStep, StepEnum newStep, String description) { return executionHandler.addSubStep(getExecutionContext(), (getExecutionContext().getJob() != null) ? getExecutionContext().getJob().getStep(parentStep) : getExecutionContext().getStep(), newStep, description); } /** * Adds a sub step on the current execution context by providing parent and new step information and map that will be resolved to create a text message that describes the new step * @param parentStep parent step to add the new sub step on * @param newStep step to add * @param valuesMap map of values that will be used to compose the description of the step */ protected Step addSubStep(StepEnum parentStep, StepEnum newStep, Map<String, String> valuesMap) { return addSubStep(parentStep, newStep, ExecutionMessageDirector.resolveStepMessage(newStep, valuesMap)); } public QuotaManager getQuotaManager() { return quotaManager; } public boolean isQuotaChanged() { return quotaChanged; } public void setQuotaChanged(boolean quotaChanged) { this.quotaChanged = quotaChanged; } @Override public void setCorrelationId(String correlationId) { // correlation ID thread local variable is set for non multi-action if (!parameters.getMultipleAction()) { CorrelationIdTracker.setCorrelationId(correlationId); } super.setCorrelationId(correlationId); } /** * Propagates an internal command failures into the command which invoked it * * @param internalReturnValue * the return value of the internal command */ protected void propagateFailure(VdcReturnValueBase internalReturnValue) { getReturnValue().getExecuteFailedMessages().addAll(internalReturnValue.getExecuteFailedMessages()); getReturnValue().setFault(internalReturnValue.getFault()); getReturnValue().getValidationMessages().addAll(internalReturnValue.getValidationMessages()); getReturnValue().setValid(internalReturnValue.isValid()); } protected void propagateFailure(VdcQueryReturnValue internalReturnValue) { getReturnValue().getExecuteFailedMessages().add(internalReturnValue.getExceptionString()); } protected VdcReturnValueBase convertToVdcReturnValueBase(final VDSReturnValue vdsReturnValue) { VdcReturnValueBase returnValue = new VdcReturnValueBase(); returnValue.setSucceeded(false); returnValue.setActionReturnValue(vdsReturnValue.getReturnValue()); String message = vdsReturnValue.getVdsError().getMessage(); returnValue.setExecuteFailedMessages(new ArrayList<>(Collections.singleton(message))); return returnValue; } public void updateCommandData() { CommandCoordinatorUtil.updateCommandData(getCommandId(), commandData); } public void persistCommand(VdcActionType parentCommand) { persistCommand(parentCommand, getContext(), getCallback() != null, callbackTriggeredByEvent()); } public void persistCommand(VdcActionType parentCommand, boolean enableCallback) { persistCommand(parentCommand, getContext(), enableCallback, callbackTriggeredByEvent()); } private boolean shouldPersistCommand() { return getCallback() != null || parentHasCallback(); } protected void persistCommandIfNeeded() { if (shouldPersistCommand()) { persistCommand(getParameters().getParentCommand()); } } protected void updateCommandIfNeeded() { if (shouldPersistCommand() && CommandCoordinatorUtil.getCommandEntity(getCommandId()) != null) { persistCommand(getParameters().getParentCommand()); } } public void persistCommand(VdcActionType parentCommand, CommandContext cmdContext, boolean enableCallback, boolean callbackWaitingForEvent) { Transaction transaction = TransactionSupport.suspend(); try { CommandEntity commandEntity = buildCommandEntity(getParentParameters(parentCommand).getCommandId(), enableCallback); commandEntity.setWaitingForEvent(callbackWaitingForEvent); CommandCoordinatorUtil.persistCommand(commandEntity, cmdContext); } finally { if (transaction != null) { TransactionSupport.resume(transaction); } } } private CommandEntity buildCommandEntity(Guid rootCommandId, boolean enableCallback) { return CommandEntity.buildCommandEntity(getUserId(), getSessionSeqId(), getCommandId(), getParameters().getParentParameters() == null ? Guid.Empty : getParameters().getParentParameters().getCommandId(), rootCommandId, buildPersistedCommandContext(), getActionType(), getParameters(), commandStatus, enableCallback, getReturnValue(), getCommandData()); } private PersistedCommandContext buildPersistedCommandContext() { PersistedCommandContext persistedCommandContext = new PersistedCommandContext(); persistedCommandContext.setJobId(getExecutionContext() == null || getExecutionContext().getJob() == null ? null : getExecutionContext().getJob().getId()); persistedCommandContext.setStepId(getExecutionContext() == null || getExecutionContext().getStep() == null ? null : getExecutionContext().getStep().getId()); persistedCommandContext.setExecutionMethod(getExecutionContext() == null ? ExecutionMethod.AsStep : getExecutionContext().getExecutionMethod()); if (getExecutionContext() != null) { persistedCommandContext.setCompleted(getExecutionContext().isCompleted()); persistedCommandContext.setJobRequired(getExecutionContext().isJobRequired()); persistedCommandContext.setMonitored(getExecutionContext().isMonitored()); persistedCommandContext.setShouldEndJob(getExecutionContext().shouldEndJob()); persistedCommandContext.setTasksMonitored(getExecutionContext().isTasksMonitored()); } return persistedCommandContext; } protected void removeCommand() { Transaction transaction = TransactionSupport.suspend(); try { CommandCoordinatorUtil.removeCommand(getCommandId()); } finally { if (transaction != null) { TransactionSupport.resume(transaction); } } } public long getSessionSeqId() { if (sessionSeqId == null) { String sessionId = getContext().getEngineContext().getSessionId(); // The session may not exists for quartz jobs sessionSeqId = getSessionDataContainer().isSessionExists(sessionId) ? getSessionDataContainer().getEngineSessionSeqId(sessionId) : SsoSessionUtils.EMPTY_SESSION_SEQ_ID; } return sessionSeqId; } public void setCommandStatus(CommandStatus status) { setCommandStatus(status, true); } public void setCommandStatus(CommandStatus status, boolean updateDB) { this.commandStatus = status; if (updateDB) { Transaction transaction = TransactionSupport.suspend(); try { CommandCoordinatorUtil.updateCommandStatus(getCommandId(), commandStatus); } finally { if (transaction != null) { TransactionSupport.resume(transaction); } } } } public void setCommandExecuted() { Transaction transaction = TransactionSupport.suspend(); try { CommandEntity cmdEntity = CommandCoordinatorUtil.getCommandEntity(getCommandId()); if (cmdEntity != null) { CommandEntity executedCmdEntity = buildCommandEntity(cmdEntity.getRootCommandId(), cmdEntity.isCallbackEnabled()); executedCmdEntity .setWaitingForEvent(cmdEntity.isCallbackEnabled() ? callbackTriggeredByEvent() : false); CommandCoordinatorUtil.persistCommand(executedCmdEntity, getContext()); CommandCoordinatorUtil.updateCommandExecuted(getCommandId()); } } finally { if (transaction != null) { TransactionSupport.resume(transaction); } } } private boolean callbackTriggeredByEvent() { CommandCallback callback = getCallback(); return callback == null ? false : callback.isTriggeredByEvent(); } public CommandStatus getCommandStatus() { return commandStatus; } public CommandCallback getCallback() { return null; } protected VdcReturnValueBase runInternalAction(VdcActionType actionType, VdcActionParametersBase parameters) { return getBackend().runInternalAction(actionType, parameters, context.clone()); } protected VdcReturnValueBase runInternalAction(VdcActionType actionType, VdcActionParametersBase parameters, CommandContext internalCommandContext) { return getBackend().runInternalAction(actionType, parameters, internalCommandContext); } protected ArrayList<VdcReturnValueBase> runInternalMultipleActions(VdcActionType actionType, ArrayList<VdcActionParametersBase> parameters) { return getBackend().runInternalMultipleActions(actionType, parameters, context.clone()); } protected ArrayList<VdcReturnValueBase> runInternalMultipleActions(VdcActionType actionType, ArrayList<VdcActionParametersBase> parameters, ExecutionContext executionContext) { return getBackend().runInternalMultipleActions(actionType, parameters, context.clone().withExecutionContext(executionContext)); } public VdcReturnValueBase runInternalActionWithTasksContext(VdcActionType actionType, VdcActionParametersBase parameters) { return runInternalActionWithTasksContext(actionType, parameters, null); } protected VdcReturnValueBase runInternalActionWithTasksContext(VdcActionType actionType, VdcActionParametersBase parameters, EngineLock lock) { return runInternalAction( actionType, parameters, ExecutionHandler.createDefaultContextForTasks(getContext(), lock)); } protected VdcQueryReturnValue runInternalQuery(VdcQueryType type, VdcQueryParametersBase queryParams) { return getBackend().runInternalQuery(type, queryParams, context.getEngineContext()); } protected CommandContext cloneContext() { return getContext().clone(); } public CommandContext cloneContextAndDetachFromParent() { return cloneContext().withoutCompensationContext().withoutExecutionContext().withoutLock(); } protected SessionDataContainer getSessionDataContainer() { return sessionDataContainer; } public VDSBrokerFrontend getVdsBroker() { return vdsBroker; } protected long getEngineSessionSeqId() { String sessionId = getParameters().getSessionId(); if (sessionId == null && getContext() != null) { sessionId = getContext().getEngineContext().getSessionId(); } if (sessionId == null) { throw new RuntimeException("No sessionId found for command " + getClass().getName()); } return getSessionDataContainer().getEngineSessionSeqId(sessionId); } /** * This method is used to return the parameters that'll determine the command that will * be called when the created async tasks will end. */ public VdcActionParametersBase getParentParameters() { // When the parent has callback the the current command parameters should always be returned as the callback is // responsible to execute the parent endAction() and not the AsyncTaskManager if (parentHasCallback()) { return getParameters(); } return getParameters().getParentParameters(); } protected <P extends VdcActionParametersBase> P withRootCommandInfo(P params, VdcActionType actionType) { VdcActionType parentCommand = isExecutedAsChildCommand() ? getParameters().getParentCommand() : actionType; params.setParentParameters(getParametersForTask(parentCommand, getParameters())); params.setParentCommand(parentCommand); return params; } protected final <P extends VdcActionParametersBase> P withRootCommandInfo(P params) { return withRootCommandInfo(params, getActionType()); } protected CommandActionState getActionState() { return actionState; } protected void subscribe(String eventKey) { CommandEntity commandEntity = buildCommandEntity(getCommandId(), true); commandEntity.setWaitingForEvent(true); CommandCoordinatorUtil.subscribe(eventKey, commandEntity); } private class DefaultCommandTransactionCompletionListener extends NoOpTransactionCompletionListener { @Override public void onRollback() { log.error("Transaction rolled-back for command '{}'.", CommandBase.this.getClass().getName()); try { if (isQuotaDependant()) { rollbackQuota(); } } catch (NullPointerException e) { log.error("RollbackQuota: failed (may be because quota is disabled)", e); } cancelTasks(); } } }