package alien4cloud.paas.wf.validation;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.collect.Lists;
import org.elasticsearch.common.collect.Maps;
import org.elasticsearch.common.collect.Sets;
import alien4cloud.paas.plan.ToscaNodeLifecycleConstants;
import alien4cloud.paas.wf.AbstractStep;
import alien4cloud.paas.wf.NodeActivityStep;
import alien4cloud.paas.wf.Path;
import alien4cloud.paas.wf.SetStateActivity;
import alien4cloud.paas.wf.Workflow;
import alien4cloud.paas.wf.WorkflowsBuilderService.TopologyContext;
import alien4cloud.paas.wf.exception.WorkflowException;
import alien4cloud.paas.wf.util.WorkflowGraphUtils;
/**
* This rule will check that for a given node, the 'set state' operations are done in the
* right order regarding the type of workflow.
* <p>
* For example, if the right sequence is known as 'create' -> 'configure' -> 'start', this means that for a given node, these 3 operations can not appear in
* another order :
* <ul>
* <li>'create' -> 'configure' -> 'start' : valid
* <li>'create' -> 'start' : valid
* <li>'configure' : valid
* <li>'create' -> 'start' -> 'configure' : invalid
* </ul>
* They must also be on the same branch (they can not be parallelized).
* <p>
* Actually the rule is: for each node, all set state steps must be <b>at least on a same path</b> and they should be in the <b>correct order</b> on this path.
* <p>
* To achieve such check, for each node we:
* <ul>
* <li>list the state step states and the paths they are found on (a step can be on several paths).
* <li>get the intersection of all these path : this way we should have at least 1 path containing all steps.
* <li>check the order on these paths.
* </ul>
* <p>
* Maybe a more efficient algo can be found (1 pass ?) ... Feel free to have fun !
*/
@Slf4j
public class StateSequenceValidation implements Rule {
private static final Map<String, Integer> INSTALL_STATES_SEQUENCE;
private static final Map<String, Integer> UNINSTALL_STATES_SEQUENCE;
static {
INSTALL_STATES_SEQUENCE = new HashMap<String, Integer>();
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.INITIAL, 0);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.CREATING, 1);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.CREATED, 2);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.CONFIGURING, 3);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.CONFIGURED, 4);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.STARTING, 5);
INSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.STARTED, 6);
UNINSTALL_STATES_SEQUENCE = new HashMap<String, Integer>();
UNINSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.STOPPING, 0);
UNINSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.STOPPED, 1);
UNINSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.DELETING, 2);
UNINSTALL_STATES_SEQUENCE.put(ToscaNodeLifecycleConstants.DELETED, 3);
}
@Override
public List<AbstractWorkflowError> validate(TopologyContext topologyContext, Workflow workflow) throws WorkflowException {
// the state sequence to use
Map<String, Integer> stateSequence = getStateSequence(workflow);
if (stateSequence == null) {
// this rule only apply on std wfs and
// needs a state sequence to run
return null;
}
if (workflow.getSteps() == null || workflow.getSteps().isEmpty()) {
return null;
}
List<AbstractWorkflowError> errors = Lists.newArrayList();
List<Path> paths = WorkflowGraphUtils.getWorkflowGraphPaths(workflow);
Map<String, Map<NodeActivityStep, Set<Path>>> pathsPerNodePerStepMap = getPathsPerNodePerStepMap(paths);
Map<String, Set<Path>> pathsPerNodeMap = getPathsPerNodeIntersectionMap(pathsPerNodePerStepMap);
// now we have to ensure that for the remaining paths, the order is correct between steps
for (Entry<String, Set<Path>> pathSetEntry : pathsPerNodeMap.entrySet()) {
String nodeId = pathSetEntry.getKey();
if (pathSetEntry.getValue().isEmpty()) {
// the intersection is empty : this means that step are in parallel
// TODO: which one ?
errors.add(new ParallelSetStatesError(nodeId));
}
for (Path path : pathSetEntry.getValue()) {
ensureOrderIsCorrect(nodeId, path, stateSequence, errors);
}
}
return errors;
}
private void ensureOrderIsCorrect(String nodeId, Path path, Map<String, Integer> stateSequence, List<AbstractWorkflowError> errors) {
Iterator<AbstractStep> steps = path.iterator();
NodeActivityStep lastDetectedStep = null;
while (steps.hasNext()) {
AbstractStep step = steps.next();
if (step instanceof NodeActivityStep && ((NodeActivityStep) step).getNodeId().equals(nodeId)
&& ((NodeActivityStep) step).getActivity() instanceof SetStateActivity) {
String stateName = ((SetStateActivity)((NodeActivityStep) step).getActivity()).getStateName();
Integer stateIdx = stateSequence.get(stateName);
if (stateIdx == null) {
// if the state is null, it can be a custom state, we don't care about it
continue;
}
if (lastDetectedStep == null) {
lastDetectedStep = (NodeActivityStep) step;
} else {
String lastDetectedState = ((SetStateActivity) ((NodeActivityStep) lastDetectedStep).getActivity()).getStateName();
Integer lastDetectedStateIdx = stateSequence.get(lastDetectedState);
Integer currentDetectedStateIdx = stateSequence.get(stateName);
if (lastDetectedStateIdx.compareTo(currentDetectedStateIdx) > 0) {
errors.add(new BadStateSequenceError(lastDetectedStep.getName(), step.getName()));
// throw new BadStateOrderException(String.format("Issue in the state sequence for node '%s': '%s' can not be set after the state '%s'",
// nodeId, stateName,
// lastDetectedState));
} else {
lastDetectedStep = (NodeActivityStep) step;
}
}
}
}
}
/**
* Per node, just keep the intersection between all the {@link Path} sets.
*/
private Map<String, Set<Path>> getPathsPerNodeIntersectionMap(Map<String, Map<NodeActivityStep, Set<Path>>> pathsPerNodePerStepMap) {
Map<String, Set<Path>> pathsPerNodeIntersectionMap = Maps.newHashMap();
for (Entry<String, Map<NodeActivityStep, Set<Path>>> entry : pathsPerNodePerStepMap.entrySet()) {
String nodeId = entry.getKey();
Map<NodeActivityStep, Set<Path>> pathsPerStep = entry.getValue();
Iterator<Set<Path>> paths = pathsPerStep.values().iterator();
if (pathsPerStep.size() == 1) {
pathsPerNodeIntersectionMap.put(nodeId, paths.next());
paths.remove();
} else {
Set<Path> lastPaths = paths.next();
paths.remove();
while (paths.hasNext()) {
Set<Path> intersection = Sets.intersection(lastPaths, paths.next());
paths.remove();
lastPaths = intersection;
}
pathsPerNodeIntersectionMap.put(nodeId, lastPaths);
}
}
return pathsPerNodeIntersectionMap;
}
/**
* For each node / step, constitute a set containing all the paths in which this node has steps of type 'set state'.
*
* @return a map using nodeId as key and the list of concerned {@link Path}s as value.
*/
private Map<String, Map<NodeActivityStep, Set<Path>>> getPathsPerNodePerStepMap(List<Path> paths) {
Map<String, Map<NodeActivityStep, Set<Path>>> pathsPerNodePerStepMap = Maps.newHashMap();
for (Path path : paths) {
Iterator<AbstractStep> steps = path.iterator();
while (steps.hasNext()) {
AbstractStep step = steps.next();
if (step instanceof NodeActivityStep && ((NodeActivityStep) step).getActivity() instanceof SetStateActivity) {
NodeActivityStep nodeActivityStep = (NodeActivityStep) step;
String node = nodeActivityStep.getNodeId();
Map<NodeActivityStep, Set<Path>> pathsPerStepMap = pathsPerNodePerStepMap.get(node);
if (pathsPerStepMap == null) {
pathsPerStepMap = Maps.newHashMap();
pathsPerNodePerStepMap.put(node, pathsPerStepMap);
}
Set<Path> pathsPerStep = pathsPerStepMap.get(nodeActivityStep);
if (pathsPerStep == null) {
pathsPerStep = Sets.newHashSet();
pathsPerStepMap.put(nodeActivityStep, pathsPerStep);
}
pathsPerStep.add(path);
}
}
}
return pathsPerNodePerStepMap;
}
private Map<String, Integer> getStateSequence(Workflow workflow) {
if (!workflow.isStandard()) {
return null;
} else if (workflow.getName().equals(Workflow.INSTALL_WF)) {
return INSTALL_STATES_SEQUENCE;
} else if (workflow.getName().equals(Workflow.UNINSTALL_WF)) {
return UNINSTALL_STATES_SEQUENCE;
} else {
return null;
}
}
}