Beispiel #1
0
    def test_set_current_task_empty_context(self):
        task = {"id": "t1", "route": 0}

        context = ctx_util.set_current_task(dict(), task)
        expected_context = {"__current_task": json_util.deepcopy(task)}

        self.assertDictEqual(context, expected_context)
    def test_set_current_task_empty_context(self):
        task = {'id': 't1', 'route': 0}

        context = ctx_util.set_current_task(dict(), task)
        expected_context = {'__current_task': copy.deepcopy(task)}

        self.assertDictEqual(context, expected_context)
Beispiel #3
0
    def test_set_current_task_nonetype_context(self):
        task = {'id': 't1', 'route': 0}

        context = ctx_util.set_current_task(None, task)
        expected_context = {'__current_task': json_util.deepcopy(task)}

        self.assertDictEqual(context, expected_context)
Beispiel #4
0
    def test_set_current_task_nonetype_context(self):
        task = {'id': 't1_1', 'name': 't1'}

        context = ctx.set_current_task(None, task)
        expected_context = {'__current_task': copy.deepcopy(task)}

        self.assertDictEqual(context, expected_context)
Beispiel #5
0
    def test_set_current_task_with_result(self):
        context = {'var1': 'foobar'}
        task = {'id': 't1_1', 'name': 't1', 'result': 'foobar'}

        context = ctx.set_current_task(context, task)
        expected_context = dict([('__current_task', copy.deepcopy(task))] + list(context.items()))

        self.assertDictEqual(context, expected_context)
    def test_set_current_task(self):
        context = {'var1': 'foobar'}
        task = {'id': 't1', 'route': 0}

        context = ctx_util.set_current_task(context, task)
        expected_context = dict([('__current_task', copy.deepcopy(task))] +
                                list(context.items()))

        self.assertDictEqual(context, expected_context)
Beispiel #7
0
    def test_set_current_task_with_result(self):
        context = {"var1": "foobar"}
        task = {"id": "t1", "route": 0, "result": "foobar"}

        context = ctx_util.set_current_task(context, task)
        expected_context = dict(
            [("__current_task", json_util.deepcopy(task))] + list(context.items())
        )

        self.assertDictEqual(context, expected_context)
Beispiel #8
0
    def make_task_context(self, task_state_entry, task_result=None):
        in_ctx_idxs = task_state_entry['ctxs']['in']
        in_ctx_val = self.get_task_context(in_ctx_idxs)

        current_task = {
            'id': task_state_entry['id'],
            'route': task_state_entry['route'],
            'result': task_result
        }

        current_ctx = ctx_util.set_current_task(in_ctx_val, current_task)
        state_ctx = {'__state': self.workflow_state.serialize()}
        current_ctx = dict_util.merge_dicts(current_ctx, state_ctx, True)

        return current_ctx
Beispiel #9
0
    def get_task(self, task_id, route):
        try:
            task_ctx = self.get_task_initial_context(task_id, route)
        except ValueError:
            task_ctx = self.get_workflow_initial_context()

        state_ctx = {'__state': self.workflow_state.serialize()}
        current_task = {'id': task_id, 'route': route}
        task_ctx = ctx_util.set_current_task(task_ctx, current_task)
        task_ctx = dict_util.merge_dicts(task_ctx, state_ctx, True)
        task_spec = self.spec.tasks.get_task(task_id).copy()
        task_spec, action_specs = task_spec.render(task_ctx)

        task = {
            'id': task_id,
            'route': route,
            'ctx': task_ctx,
            'spec': task_spec,
            'actions': action_specs
        }

        # If there is a task delay specified, evaluate the delay value.
        if getattr(task_spec, 'delay', None):
            task_delay = task_spec.delay

            if isinstance(task_delay, six.string_types):
                task_delay = expr_base.evaluate(task_delay, task_ctx)

            if not isinstance(task_delay, int):
                raise TypeError(
                    'The value of task delay is not type of integer.')

            task['delay'] = task_delay

        # Add items and related meta data to the task details.
        if task_spec.has_items():
            items_spec = getattr(task_spec, 'with')
            concurrency = getattr(items_spec, 'concurrency', None)
            task['items_count'] = len(action_specs)
            task['concurrency'] = expr_base.evaluate(concurrency, task_ctx)

        return task
Beispiel #10
0
    def get_task(self, task_id):
        task_node = self.graph.get_task(task_id)
        task_name = task_node['name']

        try:
            task_ctx = self.get_task_initial_context(task_id)['value']
        except ValueError:
            task_ctx = self.get_workflow_initial_context()

        current_task = {'id': task_id, 'name': task_name}
        task_ctx = ctx.set_current_task(task_ctx, current_task)
        task_spec = self.spec.tasks.get_task(task_name).copy()
        task_spec, action_specs = task_spec.render(task_ctx)

        task = {
            'id': task_id,
            'name': task_name,
            'ctx': task_ctx,
            'spec': task_spec,
            'actions': action_specs
        }

        # If there is a task delay specified, evaluate the delay value.
        if getattr(task_spec, 'delay', None):
            task_delay = task_spec.delay

            if isinstance(task_delay, six.string_types):
                task_delay = expr.evaluate(task_delay, task_ctx)

            if not isinstance(task_delay, int):
                raise TypeError('The value of task delay is not type of integer.')

            task['delay'] = task_delay

        # Add items and related meta data to the task details.
        if task_spec.has_items():
            items_spec = getattr(task_spec, 'with')
            task['items_count'] = len(action_specs)
            task['concurrency'] = expr.evaluate(getattr(items_spec, 'concurrency', None), task_ctx)

        return task
Beispiel #11
0
    def update_task_flow(self, task_id, event):
        in_ctx_idx = 0
        engine_event_queue = queue.Queue()

        # Throw exception if not expected event type.
        if not issubclass(type(event), events.ExecutionEvent):
            raise TypeError('Event is not type of ExecutionEvent.')

        # Throw exception if task does not exist in the workflow graph.
        if not self.graph.has_task(task_id):
            raise exc.InvalidTask(task_id)

        # Try to get the task flow entry.
        task_flow_entry = self.get_task_flow_entry(task_id)

        # Throw exception if task is not staged and there is no task flow entry.
        if task_id not in self.flow.staged and not task_flow_entry:
            raise exc.InvalidTaskFlowEntry(task_id)

        # Get the incoming context from the staged task.
        if task_id in self.flow.staged:
            in_ctx_idxs = self.flow.staged[task_id]['ctxs']

            if len(in_ctx_idxs) <= 0 or all(x == in_ctx_idxs[0] for x in in_ctx_idxs):
                in_ctx_idx = in_ctx_idxs[0]
            else:
                new_ctx_entry = self._converge_task_contexts(in_ctx_idxs)
                self.flow.contexts.append(new_ctx_entry)
                in_ctx_idx = len(self.flow.contexts) - 1

        # Create new task flow entry if it does not exist.
        if not task_flow_entry:
            task_flow_entry = self.add_task_flow(task_id, in_ctx_idx=in_ctx_idx)

        # If task is already completed and in cycle, then create new task flow entry.
        if self.graph.in_cycle(task_id) and task_flow_entry.get('state') in states.COMPLETED_STATES:
            task_flow_entry = self.add_task_flow(task_id, in_ctx_idx=in_ctx_idx)

        # Remove task from staging if task is not with items.
        if event.state and task_id in self.flow.staged and 'items' not in self.flow.staged[task_id]:
            del self.flow.staged[task_id]

        # If action execution is for a task item, then store the execution state for the item.
        if (event.state and event.context and
                'item_id' in event.context and event.context['item_id'] is not None):
            item_result = {'state': event.state, 'result': event.result}
            self.flow.staged[task_id]['items'][event.context['item_id']] = item_result

        # Log the error if it is a failed execution event.
        if event.state == states.FAILED:
            message = 'Execution failed. See result for details.'
            self.log_entry('error', message, task_id=task_id, result=event.result)

        # Process the action execution event using the task state machine and update the task state.
        old_task_state = task_flow_entry.get('state', states.UNSET)
        machines.TaskStateMachine.process_event(self, task_flow_entry, event)
        new_task_state = task_flow_entry.get('state', states.UNSET)

        # Get task result and set current context if task is completed.
        if new_task_state in states.COMPLETED_STATES:
            # Get task details required for updating outgoing context.
            task_node = self.graph.get_task(task_id)
            task_name = task_node['name']
            task_spec = self.spec.tasks.get_task(task_name)
            task_flow_idx = self._get_task_flow_idx(task_id)

            # Get task result.
            task_result = (
                [item.get('result') for item in self.flow.staged[task_id]['items']]
                if task_spec.has_items() else event.result
            )

            # Remove remaining task from staging.
            self.flow.remove_staged_task(task_id)

            # Set current task in the context.
            in_ctx_idx = task_flow_entry['ctx']
            in_ctx_val = self.flow.contexts[in_ctx_idx]['value']
            current_task = {'id': task_id, 'name': task_name, 'result': task_result}
            current_ctx = ctx.set_current_task(in_ctx_val, current_task)

            # Setup context for evaluating expressions in task transition criteria.
            flow_ctx = {'__flow': self.flow.serialize()}
            current_ctx = dx.merge_dicts(current_ctx, flow_ctx, True)

        # Evaluate task transitions if task is completed and state change is not processed.
        if new_task_state in states.COMPLETED_STATES and new_task_state != old_task_state:
            # Identify task transitions for the current completed task.
            task_transitions = self.graph.get_next_transitions(task_id)

            # Update workflow context when there is no transitions.
            if not task_transitions:
                self._update_workflow_terminal_context(in_ctx_val, task_flow_idx)

            # Iterate thru each outbound task transitions.
            for task_transition in task_transitions:
                task_transition_id = task_transition[1] + '__' + str(task_transition[2])

                # Evaluate the criteria for task transition. If there is a failure while
                # evaluating expression(s), fail the workflow.
                try:
                    criteria = task_transition[3].get('criteria') or []
                    evaluated_criteria = [expr.evaluate(c, current_ctx) for c in criteria]
                    task_flow_entry[task_transition_id] = all(evaluated_criteria)
                except Exception as e:
                    self.log_error(e, task_id, task_transition_id)
                    self.request_workflow_state(states.FAILED)
                    continue

                # If criteria met, then mark the next task staged and calculate outgoing context.
                if task_flow_entry[task_transition_id]:
                    next_task_node = self.graph.get_task(task_transition[1])
                    next_task_name = next_task_node['name']
                    next_task_id = next_task_node['id']

                    out_ctx_val, errors = task_spec.finalize_context(
                        next_task_name,
                        task_transition,
                        copy.deepcopy(current_ctx)
                    )

                    if errors:
                        self.log_errors(errors, task_id, task_transition_id)
                        self.request_workflow_state(states.FAILED)
                        continue

                    if out_ctx_val != in_ctx_val:
                        task_flow_idx = self._get_task_flow_idx(task_id)
                        self.flow.contexts.append({'srcs': [task_flow_idx], 'value': out_ctx_val})
                        out_ctx_idx = len(self.flow.contexts) - 1
                    else:
                        out_ctx_idx = in_ctx_idx

                    # Check if inbound criteria are met.
                    ready = self._inbound_criteria_satisfied(task_transition[1])

                    if (task_transition[1] in self.flow.staged and
                            'ctxs' in self.flow.staged[task_transition[1]]):
                        self.flow.staged[task_transition[1]]['ctxs'].append(out_ctx_idx)
                        self.flow.staged[task_transition[1]]['ready'] = ready
                    else:
                        staging_data = {'ctxs': [out_ctx_idx], 'ready': ready}
                        self.flow.staged[task_transition[1]] = staging_data

                    # If the next task is noop, then mark the task as completed.
                    if next_task_name in events.ENGINE_EVENT_MAP.keys():
                        engine_event_queue.put((next_task_id, next_task_name))

        # Process the task event using the workflow state machine and update the workflow state.
        task_ex_event = events.TaskExecutionEvent(task_id, task_flow_entry['state'])
        machines.WorkflowStateMachine.process_event(self, task_ex_event)

        # Process any engine commands in the queue.
        while not engine_event_queue.empty():
            next_task_id, next_task_name = engine_event_queue.get()
            engine_event = events.ENGINE_EVENT_MAP[next_task_name]
            self.update_task_flow(next_task_id, engine_event())

        # Render workflow output if workflow is completed.
        if self.get_workflow_state() in states.COMPLETED_STATES:
            in_ctx_idx = task_flow_entry['ctx']
            in_ctx_val = self.flow.contexts[in_ctx_idx]['value']
            task_flow_idx = self._get_task_flow_idx(task_id)
            self._update_workflow_terminal_context(in_ctx_val, task_flow_idx)
            self._render_workflow_outputs()

        return task_flow_entry
Beispiel #12
0
    def update_task_state(self, task_id, route, event):
        engine_event_queue = queue.Queue()

        # Throw exception if not expected event type.
        if not issubclass(type(event), events.ExecutionEvent):
            raise TypeError('Event is not type of ExecutionEvent.')

        # Throw exception if task does not exist in the workflow graph.
        if not self.graph.has_task(task_id):
            raise exc.InvalidTask(task_id)

        # Try to get the task metadata from staging or task state.
        staged_task = self.workflow_state.get_staged_task(task_id, route)
        task_state_entry = self.get_task_state_entry(task_id, route)

        # Throw exception if task is not staged and there is no task state entry.
        if not staged_task and not task_state_entry:
            raise exc.InvalidTaskStateEntry(task_id)

        # Create new task state entry if it does not exist.
        if not task_state_entry:
            task_state_entry = self.add_task_state(
                task_id,
                staged_task['route'],
                in_ctx_idxs=staged_task['ctxs']['in'],
                prev=staged_task['prev'])

        # Identify the index for the task state object for later use.
        task_state_idx = self._get_task_state_idx(task_id, route)

        # If task is already completed and in cycle, then create new task state entry.
        if (self.graph.in_cycle(task_id) and task_state_entry.get('status')
                in statuses.COMPLETED_STATUSES):
            task_state_entry = self.add_task_state(
                task_id,
                staged_task['route'],
                in_ctx_idxs=staged_task['ctxs']['in'],
                prev=staged_task['prev'])

            # Update the index value since a new entry is created.
            task_state_idx = self._get_task_state_idx(task_id, route)

        # Remove task from staging if task is not with items.
        if event.status and staged_task and 'items' not in staged_task:
            self.workflow_state.remove_staged_task(task_id, route)

        # If action execution is for a task item, then store the execution status for the item.
        if (staged_task and event.status and event.context
                and 'item_id' in event.context
                and event.context['item_id'] is not None):
            item_result = {'status': event.status, 'result': event.result}
            staged_task['items'][event.context['item_id']] = item_result

        # Log the error if it is a failed execution event.
        if event.status == statuses.FAILED:
            message = 'Execution failed. See result for details.'
            self.log_entry('error',
                           message,
                           task_id=task_id,
                           result=event.result)

        # Process the action execution event using the
        # task state machine and update the task status.
        old_task_status = task_state_entry.get('status', statuses.UNSET)
        machines.TaskStateMachine.process_event(self.workflow_state,
                                                task_state_entry, event)
        new_task_status = task_state_entry.get('status', statuses.UNSET)

        # Get task result and set current context if task is completed.
        if new_task_status in statuses.COMPLETED_STATUSES:
            # Get task details required for updating outgoing context.
            task_spec = self.spec.tasks.get_task(task_id)

            # Get task result.
            task_result = ([
                item.get('result') for item in staged_task.get('items', [])
            ] if staged_task and task_spec.has_items() else event.result)

            # Remove remaining task from staging.
            self.workflow_state.remove_staged_task(task_id, route)

            # Set current task in the context.
            in_ctx_idxs = task_state_entry['ctxs']['in']
            in_ctx_val = self.get_task_context(in_ctx_idxs)
            current_task = {
                'id': task_id,
                'route': route,
                'result': task_result
            }
            current_ctx = ctx_util.set_current_task(in_ctx_val, current_task)

            # Setup context for evaluating expressions in task transition criteria.
            state_ctx = {'__state': self.workflow_state.serialize()}
            current_ctx = dict_util.merge_dicts(current_ctx, state_ctx, True)

        # Evaluate task transitions if task is completed and status change is not processed.
        if new_task_status in statuses.COMPLETED_STATUSES and new_task_status != old_task_status:
            has_manual_fail = False
            staged_next_tasks = []

            # Identify task transitions for the current completed task.
            task_transitions = self.graph.get_next_transitions(task_id)

            # Mark task as terminal when there is no transitions.
            if not task_transitions:
                task_state_entry['term'] = True

            # Iterate thru each outbound task transitions.
            for task_transition in task_transitions:
                task_transition_id = (
                    constants.TASK_STATE_TRANSITION_FORMAT %
                    (task_transition[1], str(task_transition[2])))

                # Evaluate the criteria for task transition. If there is a failure while
                # evaluating expression(s), fail the workflow.
                try:
                    criteria = task_transition[3].get('criteria') or []
                    evaluated_criteria = [
                        expr_base.evaluate(c, current_ctx) for c in criteria
                    ]
                    task_state_entry['next'][task_transition_id] = all(
                        evaluated_criteria)
                except Exception as e:
                    self.log_error(e, task_id, route, task_transition_id)
                    self.request_workflow_status(statuses.FAILED)
                    continue

                # If criteria met, then mark the next task staged and calculate outgoing context.
                if task_state_entry['next'][task_transition_id]:
                    next_task_node = self.graph.get_task(task_transition[1])
                    next_task_id = next_task_node['id']
                    new_ctx_idx = None

                    # Get and process new context for the task transition.
                    out_ctx, new_ctx, errors = task_spec.finalize_context(
                        next_task_id, task_transition,
                        copy.deepcopy(current_ctx))

                    if errors:
                        self.log_errors(errors, task_id, route,
                                        task_transition_id)
                        self.request_workflow_status(statuses.FAILED)
                        continue

                    out_ctx_idxs = copy.deepcopy(
                        task_state_entry['ctxs']['in'])

                    if new_ctx:
                        self.workflow_state.contexts.append(new_ctx)
                        new_ctx_idx = len(self.workflow_state.contexts) - 1

                        # Add to the list of contexts for the next task in this transition.
                        out_ctx_idxs.append(new_ctx_idx)

                        # Record the outgoing context for this task transition.
                        if 'out' not in task_state_entry['ctxs']:
                            task_state_entry['ctxs']['out'] = {}

                        task_state_entry['ctxs']['out'] = {
                            task_transition_id: new_ctx_idx
                        }

                    # Stage the next task if it is not in staging.
                    next_task_route = self._evaluate_route(
                        task_transition, route)

                    staged_next_task = self.workflow_state.get_staged_task(
                        next_task_id, next_task_route)

                    backref = (constants.TASK_STATE_TRANSITION_FORMAT %
                               (task_id, str(task_transition[2])))

                    # If the next task is already staged.
                    if staged_next_task:
                        # Remove the root context to avoid overwriting vars.
                        out_ctx_idxs.remove(0)

                        # Extend the outgoing context from this task.
                        staged_next_task['ctxs']['in'].extend(out_ctx_idxs)

                        # Add a backref for the current task in the next task.
                        staged_next_task['prev'][backref] = task_state_idx
                    else:
                        # Otherwise create a new entry in staging for the next task.
                        staged_next_task = self.workflow_state.add_staged_task(
                            next_task_id,
                            next_task_route,
                            ctxs=out_ctx_idxs,
                            prev={backref: task_state_idx},
                            ready=False)

                    # Check if inbound criteria are met. Must use the original route
                    # to identify the inbound task transitions.
                    staged_next_task[
                        'ready'] = self._inbound_criteria_satisfied(
                            next_task_id, route)

                    # Put the next task in the engine event queue if it is an engine command.
                    if next_task_id in events.ENGINE_EVENT_MAP.keys():
                        queue_entry = (staged_next_task['id'],
                                       staged_next_task['route'])
                        engine_event_queue.put(queue_entry)

                        # Flag if there is at least one fail command in the task transition.
                        if not has_manual_fail:
                            has_manual_fail = (next_task_id == 'fail')
                    else:
                        # If not an engine command and the next task is ready, then
                        # make a record of it for processing manual fail below.
                        if staged_next_task['ready']:
                            staged_next_tasks.append(staged_next_task)

            # Task failure is remediable. For example, there may be workflow that wants
            # to run a cleanup task on failure. In certain cases, we still want to fail
            # the workflow after the remediation. The fail command can be in the
            # task transition under the cleanup task. However, the cleanup task may be
            # reusable when either workflow succeed or fail. It does not make sense to
            # put the fail command in the task transition of the cleanup task. So we
            # want to allow user to be able to define both the cleanup and fail in the
            # task transition under the current task but still return the cleanup task
            # even when the workflow is set to failed status.
            if has_manual_fail:
                for staged_next_task in staged_next_tasks:
                    staged_next_task['run_on_fail'] = True

        # Process the task event using the workflow state machine and update the workflow status.
        task_ex_event = events.TaskExecutionEvent(task_id, route,
                                                  task_state_entry['status'])
        machines.WorkflowStateMachine.process_event(self.workflow_state,
                                                    task_ex_event)

        # Process any engine commands in the queue.
        while not engine_event_queue.empty():
            next_task_id, next_task_route = engine_event_queue.get()
            engine_event = events.ENGINE_EVENT_MAP[next_task_id]
            self.update_task_state(next_task_id, next_task_route,
                                   engine_event())

        # Render workflow output if workflow is completed.
        if self.get_workflow_status() in statuses.COMPLETED_STATUSES:
            task_state_entry['term'] = True
            self._render_workflow_outputs()

        return task_state_entry
Beispiel #13
0
    def assert_conducting_sequences(self,
                                    wf_name,
                                    expected_task_seq,
                                    inputs=None,
                                    mock_states=None,
                                    mock_results=None,
                                    expected_workflow_state=None,
                                    expected_output=None):
        if inputs is None:
            inputs = {}

        wf_def = self.get_wf_def(wf_name)
        wf_spec = self.spec_module.instantiate(wf_def)
        conductor = conducting.WorkflowConductor(wf_spec, inputs=inputs)
        conductor.request_workflow_state(states.RUNNING)

        context = {}
        q = queue.Queue()
        state_q = queue.Queue()
        result_q = queue.Queue()

        if mock_states:
            for item in mock_states:
                state_q.put(item)

        if mock_results:
            for item in mock_results:
                result_q.put(item)

        # Get start tasks and being conducting workflow.
        for task in conductor.get_next_tasks():
            q.put(task)

        # Serialize workflow conductor to mock async execution.
        wf_conducting_state = conductor.serialize()

        while not q.empty():
            current_task = q.get()
            current_task_id = current_task['id']

            # Deserialize workflow conductor to mock async execution.
            conductor = conducting.WorkflowConductor.deserialize(
                wf_conducting_state)

            # Set task state to running.
            ac_ex_event = events.ActionExecutionEvent(states.RUNNING)
            conductor.update_task_flow(current_task_id, ac_ex_event)

            # Set current task in context.
            context = ctx.set_current_task(context, current_task)

            # Mock completion of the task.
            state = state_q.get() if not state_q.empty() else states.SUCCEEDED
            result = result_q.get() if not result_q.empty() else None
            ac_ex_event = events.ActionExecutionEvent(state, result=result)
            conductor.update_task_flow(current_task_id, ac_ex_event)

            # Identify the next set of tasks.
            next_tasks = conductor.get_next_tasks(current_task_id)

            for next_task in next_tasks:
                q.put(next_task)

            # Serialize workflow execution graph to mock async execution.
            wf_conducting_state = conductor.serialize()

        self.assertListEqual(
            expected_task_seq,
            [entry['id'] for entry in conductor.flow.sequence])

        if expected_workflow_state is not None:
            self.assertEqual(conductor.get_workflow_state(),
                             expected_workflow_state)

        if expected_output is not None:
            self.assertDictEqual(conductor.get_workflow_output(),
                                 expected_output)