예제 #1
0
    def assert_workflow_with_single_item(self, ac_ex_status, tk_ex_status, wf_ex_status):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = 'task1'
        task_ctx = {'xs': ['fee']}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
        ]

        # Verify the set of action executions.
        expected_task = self.format_task_item(
            task_name,
            task_route,
            task_ctx,
            conductor.spec.tasks.get_task(task_name),
            actions=task_action_specs,
            items_count=len(task_ctx['xs'])
        )

        expected_tasks = [expected_task]
        actual_tasks = conductor.get_next_tasks()
        self.assert_task_list(conductor, actual_tasks, expected_tasks)

        # Set the item to running status.
        self.forward_task_statuses(conductor, task_name, [statuses.RUNNING], [0])

        # Assert that the task is running.
        actual_task_status = conductor.workflow_state.get_task(task_name, task_route)['status']
        self.assertEqual(actual_task_status, statuses.RUNNING)

        # Change status for the item.
        item_ids = [0]
        results = [task_ctx['xs'][0]]
        status_changes = [ac_ex_status]
        self.forward_task_statuses(conductor, task_name, status_changes, item_ids, results)

        # Assert task and workflow status.
        actual_task_status = conductor.workflow_state.get_task(task_name, task_route)['status']
        self.assertEqual(actual_task_status, tk_ex_status)
        self.assertEqual(conductor.get_workflow_status(), wf_ex_status)
예제 #2
0
    def test_workflow_not_in_rerunable_status(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.echo message="$RANDOM"
            next:
              - when: <% succeeded() %>
                do: task2
          task2:
            action: core.noop
        """

        fast_forward_success = [statuses.RUNNING, statuses.SUCCEEDED]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Succeed task1.
        next_tasks = conductor.get_next_tasks()
        self.forward_task_statuses(conductor, next_tasks[0]['id'], fast_forward_success)

        # Assert rerun cannot happen because workflow is still running.
        self.assertRaises(
            exc.WorkflowIsActiveAndNotRerunableError,
            conductor.request_workflow_rerun
        )
    def test_init_task_with_no_action(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            next:
              - publish: xyz=123
                do: task2
          task2:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Process task1.
        task_name = "task1"
        expected_task_ctx = {}
        self.assert_next_task(conductor, task_name, expected_task_ctx)
        self.forward_task_statuses(conductor, task_name,
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # Process task2.
        task_name = "task2"
        expected_task_ctx = {"xyz": 123}
        self.assert_next_task(conductor, task_name, expected_task_ctx)
        self.forward_task_statuses(conductor, task_name,
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
예제 #4
0
    def test_task_delay_rendering_bad_type(self):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        vars:
          - delay: foobar

        tasks:
          task1:
            delay: <% ctx().delay %>
            action: core.noop
        """

        expected_errors = [{
            "type": "error",
            "message":
            "TypeError: The value of task delay is not type of integer.",
            "task_id": "task1",
            "route": 0,
        }]

        # Instantiate workflow spec.
        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Assert failed status and errors.
        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
예제 #5
0
    def test_get_task_status_of_tasks_along_splits(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task2
              - when: <% failed() %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
              - when: <% failed() %>
                do: task3
          task3:
            action: core.noop
            next:
              - publish:
                  - task1_status: <% task_status(task1) %>
                  - task2_status: <% task_status(task2) %>
                  - task3_status: <% task_status(task3) %>

        output:
          - task1_status: <% ctx(task1_status) %>
          - task2_status: <% ctx(task2_status) %>
          - task3_status: <% ctx(task3_status) %>
        """

        expected_errors = []
        expected_output = {
            "task1_status": "succeeded",
            "task2_status": "succeeded",
            "task3_status": "succeeded",
        }

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.assertListEqual(conductor.errors, expected_errors)

        # Complete tasks
        status_changes = [statuses.RUNNING, statuses.SUCCEEDED]
        self.forward_task_statuses(conductor, "task1", status_changes)
        self.forward_task_statuses(conductor, "task2", status_changes, route=1)
        self.forward_task_statuses(conductor, "task3", status_changes, route=2)

        # Render workflow output and check workflow status and output.
        conductor.render_workflow_output()
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
예제 #6
0
    def assert_workflow_state(self,
                              wf_name,
                              mock_flow,
                              expected_wf_states,
                              conductor=None):
        if not conductor:
            wf_def = self.get_wf_def(wf_name)
            wf_spec = self.spec_module.instantiate(wf_def)
            conductor = conducting.WorkflowConductor(wf_spec)
            conductor.request_workflow_state(states.RUNNING)

        for task_flow_entry, expected_wf_state in zip(mock_flow,
                                                      expected_wf_states):
            task_id = task_flow_entry['id']
            task_state = task_flow_entry['state']
            ac_ex_event = events.ActionExecutionEvent(task_state)
            conductor.update_task_flow(task_id, ac_ex_event)

            err_ctx = ('Workflow state "%s" is not the expected state "%s". '
                       'Updated task "%s" with state "%s".' %
                       (conductor.get_workflow_state(), expected_wf_state,
                        task_id, task_state))

            self.assertEqual(conductor.get_workflow_state(), expected_wf_state,
                             err_ctx)

        return conductor
    def test_runtime_function_of_items_list_size(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: <% range(500).select(str($)) %>

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        num_items = 500

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # Mock the action execution for each item and assert expected task states.
        task_name = 'task1'
        task_ctx = {'xs': [str(i) for i in range(0, num_items)]}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': i}, 'item_id': int(i)}
            for i in task_ctx['xs']
        ]

        mock_ac_ex_states = [states.SUCCEEDED] * num_items
        expected_task_states = [states.RUNNING] * (num_items - 1) + [states.SUCCEEDED]
        expected_workflow_states = [states.RUNNING] * (num_items - 1) + [states.SUCCEEDED]

        self.assert_task_items(
            conductor,
            task_name,
            task_ctx,
            task_ctx['xs'],
            task_action_specs,
            mock_ac_ex_states,
            expected_task_states,
            expected_workflow_states
        )

        # Assert the task is removed from staging.
        self.assertNotIn(task_name, conductor.flow.staged)

        # Assert the workflow succeeded.
        self.assertEqual(conductor.get_workflow_state(), states.SUCCEEDED)

        # Assert the workflow output is correct.
        expected_output = {'items': task_ctx['xs']}
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
예제 #8
0
    def _prep_conductor(self, status=None):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          task3:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)

        if status:
            conductor.request_workflow_status(status)

        return conductor
    def test_cancel_workflow_already_canceling(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow and keep it running.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])

        # Cancels the workflow and complete task1.
        conductor.request_workflow_status(statuses.CANCELING)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)

        # Cancels the workflow again.
        conductor.request_workflow_status(statuses.CANCELING)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)
        conductor.request_workflow_status(statuses.CANCELED)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)

        # Complete task1 and check workflow status.
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELED)
    def test_workflow_output(self):
        wf_def = """
        version: 1.0

        output:
          - x: 123
          - y: <% ctx().x %>

        tasks:
          task1:
            action: core.noop
        """

        expected_output = {'x': 123, 'y': 123}

        expected_errors = []

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow and keep it running.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])

        # Cancels the workflow and complete task1.
        conductor.request_workflow_status(statuses.CANCELING)
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])

        # Check workflow status and output.
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
    def test_workflow_vars_error(self):
        wf_def = """
        version: 1.0

        vars:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% result().foobar %>'. ExpressionEvaluationException: "
             "The current task is not set in the context."),
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
예제 #12
0
    def assert_workflow_status(self,
                               wf_name,
                               mock_flow,
                               expected_wf_statuses,
                               conductor=None):
        if not conductor:
            wf_def = self.get_wf_def(wf_name)
            wf_spec = self.spec_module.instantiate(wf_def)
            conductor = conducting.WorkflowConductor(wf_spec)
            conductor.request_workflow_status(statuses.RUNNING)

        for _entry, expected_wf_status in zip(mock_flow, expected_wf_statuses):
            task_id = _entry['id']
            task_status = _entry['status']

            self.forward_task_statuses(conductor, task_id, [task_status])

            err_ctx = ('Workflow status "%s" is not the expected status "%s". '
                       'Updated task "%s" with status "%s".' %
                       (conductor.get_workflow_status(), expected_wf_status,
                        task_id, task_status))

            self.assertEqual(conductor.get_workflow_status(),
                             expected_wf_status, err_ctx)

        return conductor
    def test_workflow_input_error(self):
        wf_def = """
        version: 1.0

        input:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            'type':
            'error',
            'message':
            ('YaqlEvaluationException: Unable to evaluate expression '
             '\'<% result().foobar %>\'. ExpressionEvaluationException: '
             'The current task is not set in the context.')
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assertListEqual(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_pause_and_failed_with_task_transition_error(self):
        wf_def = """
        version: 1.0
        description: A basic sequential workflow.
        tasks:
          task1:
            action: core.noop
            next:
              - when: <% result().foobar %>
                do: task2
          task2:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Run task1.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)

        # Complete task1 and assert the workflow execution fails
        # due to the expression error in the task transition.
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
    def test_workflow_vars_seq_ref_error(self):
        wf_def = """
        version: 1.0

        vars:
          - x: 123
          - y: <% ctx().x %>
          - z: <% ctx().y.value %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                )
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        self.assertListEqual(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_bad_items_type(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: fee fi fo fum

        tasks:
          task1:
            with: x in <% ctx(xs) %>
            action: core.echo message=<% item(y) %>
        """

        expected_errors = [{
            'type': 'error',
            'message':
            'TypeError: The value of "<% ctx(xs) %>" is not type of list.',
            'task_id': 'task1'
        }]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)
        tasks = conductor.get_next_tasks()

        self.assertListEqual(tasks, [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
예제 #17
0
    def test_task_delay_rendering_bad_type(self):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        vars:
          - delay: foobar

        tasks:
          task1:
            delay: <% ctx().delay %>
            action: core.noop
        """

        expected_errors = [{
            'type': 'error',
            'message':
            'TypeError: The value of task delay is not type of integer.',
            'task_id': 'task1'
        }]

        # Instantiate workflow spec.
        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # Assert failed status and errors.
        self.assert_task_list(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_get_start_tasks_via_get_next_tasks_with_multiple_task_action_and_input_errors(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar
          - fubar: foobar

        tasks:
          task1:
            action: <% ctx().foobar.fubar %>
            next:
              - when: <% succeeded() %>
                do: task3
          task2:
            action: core.noop var_x=<% ctx().fubar.foobar %>
            next:
              - when: <% succeeded() %>
                do: task3
          task3:
            join: all
            action: core.noop
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().foobar.fubar %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#fubar"'
                ),
                'task_id': 'task1'
            },
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().fubar.foobar %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#foobar"'
                ),
                'task_id': 'task2'
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # The get_next_tasks method should not return any tasks.
        self.assertListEqual(conductor.get_next_tasks(), [])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
    def test_workflow_input_seq_ref_error(self):
        wf_def = """
        version: 1.0

        input:
          - x
          - y: <% ctx().x %>
          - z: <% ctx().y.value %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% ctx().y.value %>'. NoFunctionRegisteredException: "
             'Unknown function "#property#value"'),
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_run_command_task_is_failed(self):
        # initialize workflow conductor
        conductor = conducting.WorkflowConductor(**{
            'spec': self.spec,
            'inputs': {
                'cmd': 'test_command',
                'host': 'test.example.com',
                'slack_channel': '#hoge'
            },
        })

        # initialize workflow status
        conductor.request_workflow_status(statuses.RUNNING)

        # get task informations to be run at first
        next_tasks = conductor.get_next_tasks()

        # finish run_command with failure and get next task
        self.forward_task_statuses(conductor, 'run_command', [statuses.RUNNING, statuses.FAILED])
        next_tasks = conductor.get_next_tasks()

        self.assertEqual(len(next_tasks), 1)
        self.assertEqual(next_tasks[0]['id'], 'report_error')

        self.assertEqual(len(next_tasks[0]['actions']), 1)
        _action_info = next_tasks[0]['actions'][0]
        self.assertEqual(_action_info['action'], 'slack.post_message')
        self.assertEqual(_action_info['input']['channel'], '#hoge')
        self.assertEqual(_action_info['input']['message'], (
            '===[   ERROR   ]===\n'
            'run_command task was failed to run command '
            '("test_command") on test.example.com\n'
        ))
    def test_workflow_output_error(self):
        wf_def = """
        version: 1.0

        output:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            'type':
            'error',
            'message':
            ('YaqlEvaluationException: Unable to evaluate expression '
             '\'<% result().foobar %>\'. ExpressionEvaluationException: '
             'The current task is not set in the context.')
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Manually complete task1.
        self.forward_task_statuses(conductor, 'task1',
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertIsNone(conductor.get_workflow_output())
    def test_task_transition_publish_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var1: <% ctx().foobar.fubar %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% ctx().foobar.fubar %>'. NoFunctionRegisteredException: "
             'Unknown function "#property#fubar"'),
            "route":
            0,
            "task_id":
            "task1",
            "task_transition_id":
            "task2__t0",
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        self.forward_task_statuses(conductor, "task1",
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        actual_errors = sorted(conductor.errors,
                               key=lambda x: x.get("task_id", None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn("task2", conductor.workflow_state.staged)

        # Since the workflow failed, there should be no next tasks returned.
        self.assert_next_task(conductor, has_next_task=False)
    def test_fail_one_and_only_item(self):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = "task1"
        task_ctx = {"xs": ["fee"]}

        task_action_specs = [
            {
                "action": "core.echo",
                "input": {
                    "message": "fee"
                },
                "item_id": 0
            },
        ]

        mock_ac_ex_statuses = [statuses.FAILED]
        expected_task_statuses = [statuses.FAILED]
        expected_workflow_statuses = [statuses.FAILED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx["xs"],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses,
        )

        # Assert the task is not removed from staging. This is intentional so the with items
        # task can be rerun partially for failed items or items that hasn't been run.
        self.assertIsNotNone(
            conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow failed.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
    def test_empty_items_list(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: []

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = "task1"
        task_ctx = {"xs": []}
        task_action_specs = []

        mock_ac_ex_statuses = []
        expected_task_statuses = [statuses.SUCCEEDED]
        expected_workflow_statuses = [statuses.SUCCEEDED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx["xs"],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses,
        )

        # Assert the task is removed from staging.
        self.assertIsNone(
            conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow succeeded.
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)

        # Assert the workflow output is correct.
        conductor.render_workflow_output()
        expected_output = {"items": []}
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
    def test_run_command_task_is_succeeded(self):
        # initialize workflow conductor
        conductor = conducting.WorkflowConductor(**{
            'spec': self.spec,
            'inputs': {
                'cmd': 'test_command',
                'host': 'test.example.com',
                'slack_channel': '#hoge'
            },
        })

        # initialize workflow status
        conductor.request_workflow_status(statuses.RUNNING)

        # get task informations to be run at first
        next_tasks = conductor.get_next_tasks()

        # This confirms whether expected action is specified
        self.assertEqual(len(next_tasks), 1)
        self.assertEqual(next_tasks[0]['id'], 'run_command')

        # When 'with' parameter is specified in action, 'actions' might have params more than 1.
        self.assertEqual(len(next_tasks[0]['actions']), 1)
        _action_info = next_tasks[0]['actions'][0]
        self.assertEqual(_action_info['action'], 'core.remote')
        self.assertEqual(_action_info['input']['cmd'], 'test_command')
        self.assertEqual(_action_info['input']['hosts'], 'test.example.com')

        # finish run_command with successful
        self.forward_task_statuses(conductor, 'run_command', [statuses.RUNNING])
        self.forward_task_statuses(conductor, 'run_command', [statuses.SUCCEEDED], **{
            'result': {
                'test.example.com': {
                    'stdout': 'test_output'
                }
            }
        })

        # get next running task after run_command is finished successfully
        next_tasks = conductor.get_next_tasks()

        # This confirms whether expected action is specified
        self.assertEqual(len(next_tasks), 1)
        self.assertEqual(next_tasks[0]['id'], 'report_result')

        self.assertEqual(len(next_tasks[0]['actions']), 1)
        _action_info = next_tasks[0]['actions'][0]
        self.assertEqual(_action_info['action'], 'slack.post_message')
        self.assertEqual(_action_info['input']['channel'], '#hoge')
        self.assertEqual(_action_info['input']['message'], (
            '===[ SUCCEEDED ]===\n'
            '* (command) "test_command" on test.example.com\n'
            '* (output) test_output\n'
        ))

        # This confirms after finishing report_result there is no task to be run
        self.forward_task_statuses(conductor, 'report_result', [statuses.RUNNING, statuses.SUCCEEDED])
        self.assertEqual(conductor.get_next_tasks(), [])
예제 #26
0
    def test_task_transition_publish_seq_ref_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - x: 123
                  - y: <% ctx().x %>
                  - z: <% ctx().y.value %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                ),
                'route': 0,
                'task_id': 'task1',
                'task_transition_id': 'task2__t0'
            }
        ]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING, statuses.SUCCEEDED])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn('task2', conductor.workflow_state.staged)

        # Since the workflow failed, there should be no next tasks returned.
        self.assert_next_task(conductor, has_next_task=False)
예제 #27
0
    def test_with_items_rendering(self):
        wf_def = """
        version: 1.0

        input:
          - xs
          - concurrency

        tasks:
          task1:
            with:
              items: <% ctx(xs) %>
              concurrency: <% ctx(concurrency) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        # Instantiate workflow spec.
        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        inputs = {'xs': ['fee', 'fi', 'fo', 'fum'], 'concurrency': 2}
        conductor = conducting.WorkflowConductor(spec, inputs=inputs)
        conductor.request_workflow_status(statuses.RUNNING)

        # Test that the items and context of a task is rendered from context.
        task_route = 0
        next_task_name = 'task1'
        next_task_ctx = inputs
        next_task_spec = conductor.spec.tasks.get_task(next_task_name)

        next_task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
            {'action': 'core.echo', 'input': {'message': 'fi'}, 'item_id': 1},
            {'action': 'core.echo', 'input': {'message': 'fo'}, 'item_id': 2},
            {'action': 'core.echo', 'input': {'message': 'fum'}, 'item_id': 3},
        ]

        expected_task = self.format_task_item(
            next_task_name,
            task_route,
            next_task_ctx,
            next_task_spec,
            actions=next_task_action_specs[0:inputs['concurrency']],
            items_count=len(inputs['xs']),
            items_concurrency=inputs['concurrency']
        )

        expected_tasks = [expected_task]

        self.assert_task_list(conductor, conductor.get_next_tasks(), expected_tasks)
    def test_task_transition_publish_seq_ref_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - x: 123
                  - y: <% ctx().x %>
                  - z: <% ctx().y.value %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                ),
                'task_id': 'task1',
                'task_transition_id': 'task2__0'
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        task_name = 'task1'
        conductor.update_task_flow(task_name, events.ActionExecutionEvent(states.RUNNING))
        conductor.update_task_flow(task_name, events.ActionExecutionEvent(states.SUCCEEDED))

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn('task2', conductor.flow.staged)
        self.assertListEqual(conductor.get_next_tasks(), [])
예제 #29
0
    def test_pause_workflow_already_pausing(self):
        wf_def = """
        version: 1.0
        description: A basic branching workflow.
        tasks:
          # branch 1
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # branch 2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # adjoining branch
          task3:
            join: all
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Run task1 and task2.
        self.forward_task_statuses(conductor, "task1", [statuses.RUNNING])
        self.forward_task_statuses(conductor, "task2", [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)

        # Pause the workflow.
        conductor.request_workflow_status(statuses.PAUSING)

        # Complete task1 only. The workflow should still be pausing
        # because task2 is still running.
        self.forward_task_statuses(conductor, "task1", [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)

        # Pause the workflow.
        conductor.request_workflow_status(statuses.PAUSING)
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)
        conductor.request_workflow_status(statuses.PAUSED)
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)

        # Complete task2. When task2 completes, the workflow should be paused
        # because there is no task in active status.
        self.forward_task_statuses(conductor, "task2", [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSED)

        # Resume the workflow, task3 should be staged, and complete task3.
        conductor.request_workflow_status(statuses.RESUMING)
        self.assert_next_task(conductor, "task3", {})
        self.forward_task_statuses(conductor, "task3",
                                   [statuses.RUNNING, statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
    def test_failed_item_task_dormant_other_failed(self):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee
              - fi
              - fo
              - fum

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = 'task1'
        task_ctx = {'xs': ['fee', 'fi', 'fo', 'fum']}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
            {'action': 'core.echo', 'input': {'message': 'fi'}, 'item_id': 1},
            {'action': 'core.echo', 'input': {'message': 'fo'}, 'item_id': 2},
            {'action': 'core.echo', 'input': {'message': 'fum'}, 'item_id': 3},
        ]

        mock_ac_ex_statuses = [statuses.SUCCEEDED, statuses.FAILED, statuses.FAILED]
        expected_task_statuses = [statuses.RUNNING, statuses.RUNNING, statuses.FAILED]
        expected_workflow_statuses = [statuses.RUNNING, statuses.RUNNING, statuses.FAILED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx['xs'],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses
        )

        # Assert the task is not removed from staging. This is intentional so the with items
        # task can be rerun partially for failed items or items that hasn't been run.
        self.assertIsNotNone(conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow failed.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)