Exemple #1
0
    def _register_action(self, pack, action):
        content = self._meta_loader.load(action)
        pack_field = content.get('pack', None)
        if not pack_field:
            content['pack'] = pack
            pack_field = pack
        if pack_field != pack:
            raise Exception(
                'Model is in pack "%s" but field "pack" is different: %s' %
                (pack, pack_field))

        action_api = ActionAPI(**content)
        action_api.validate()
        action_validator.validate_action(action_api)
        model = ActionAPI.to_model(action_api)

        action_ref = ResourceReference.to_string_reference(
            pack=pack, name=str(content['name']))
        existing = action_utils.get_action_by_ref(action_ref)
        if not existing:
            LOG.debug('Action %s not found. Creating new one with: %s',
                      action_ref, content)
        else:
            LOG.debug('Action %s found. Will be updated from: %s to: %s',
                      action_ref, existing, model)
            model.id = existing.id

        try:
            model = Action.add_or_update(model)
            extra = {'action_db': model}
            LOG.audit('Action updated. Action %s from %s.',
                      model,
                      action,
                      extra=extra)
        except Exception:
            LOG.exception('Failed to write action to db %s.', model.name)
            raise
Exemple #2
0
def request_pause(liveaction, requester):
    """
    Request pause for a running action execution.

    :return: (liveaction, execution)
    :rtype: tuple
    """
    # Validate that the runner type of the action supports pause.
    action_db = action_utils.get_action_by_ref(liveaction.action)

    if not action_db:
        raise ValueError(
            'Unable to pause liveaction "%s" because the action "%s" '
            'is not found.' % (liveaction.id, liveaction.action))

    if action_db.runner_type[
            'name'] not in action_constants.WORKFLOW_RUNNER_TYPES:
        raise runner_exc.InvalidActionRunnerOperationError(
            'Unable to pause liveaction "%s" because it is not supported by the '
            '"%s" runner.' % (liveaction.id, action_db.runner_type['name']))

    if (liveaction.status == action_constants.LIVEACTION_STATUS_PAUSING
            or liveaction.status == action_constants.LIVEACTION_STATUS_PAUSED):
        execution = ActionExecution.get(liveaction__id=str(liveaction.id))
        return (liveaction, execution)

    if liveaction.status != action_constants.LIVEACTION_STATUS_RUNNING:
        raise runner_exc.UnexpectedActionExecutionStatusError(
            'Unable to pause liveaction "%s" because it is not in a running state.'
            % liveaction.id)

    liveaction = update_status(liveaction,
                               action_constants.LIVEACTION_STATUS_PAUSING)

    execution = ActionExecution.get(liveaction__id=str(liveaction.id))

    return (liveaction, execution)
Exemple #3
0
    def test_chained_executions(self):
        liveaction = LiveActionDB(action="executions.chain")
        liveaction, _ = action_service.request(liveaction)
        liveaction = self._wait_on_status(
            liveaction, action_constants.LIVEACTION_STATUS_FAILED)

        execution = self._get_action_execution(liveaction__id=str(
            liveaction.id),
                                               raise_exception=True)

        action = action_utils.get_action_by_ref("executions.chain")
        self.assertDictEqual(execution.action,
                             vars(ActionAPI.from_model(action)))
        runner = RunnerType.get_by_name(action.runner_type["name"])
        self.assertDictEqual(execution.runner,
                             vars(RunnerTypeAPI.from_model(runner)))
        liveaction = LiveAction.get_by_id(str(liveaction.id))
        self.assertEqual(execution.start_timestamp, liveaction.start_timestamp)
        # NOTE: Timestamp of liveaction and execution may be a bit different, depending on how long
        # it takes to persist each object in the database
        self.assertEqual(
            execution.end_timestamp.replace(microsecond=0),
            liveaction.end_timestamp.replace(microsecond=0),
        )
        self.assertEqual(execution.result, liveaction.result)
        self.assertEqual(execution.status, liveaction.status)
        self.assertEqual(execution.context, liveaction.context)
        self.assertEqual(execution.liveaction["callback"], liveaction.callback)
        self.assertEqual(execution.liveaction["action"], liveaction.action)
        self.assertGreater(len(execution.children), 0)

        for child in execution.children:
            record = ActionExecution.get(id=child, raise_exception=True)
            self.assertEqual(record.parent, str(execution.id))
            self.assertEqual(record.action["name"], "local")
            self.assertEqual(record.runner["name"], "local-shell-cmd")
Exemple #4
0
    def dispatch(self, liveaction_db):
        action_db = get_action_by_ref(liveaction_db.action)
        if not action_db:
            raise Exception('Action %s not found in DB.' % (liveaction_db.action))

        runnertype_db = get_runnertype_by_name(action_db.runner_type['name'])

        extra = {'liveaction_db': liveaction_db, 'runnertype_db': runnertype_db}
        LOG.info('Dispatching Action to a runner', extra=extra)

        # Get runner instance.
        runner = get_runner(runnertype_db.runner_module)
        LOG.debug('Runner instance for RunnerType "%s" is: %s', runnertype_db.name, runner)

        # Invoke pre_run, run, post_run cycle.
        liveaction_db = self._do_run(runner, runnertype_db, action_db, liveaction_db)

        extra = {'result': liveaction_db.result}
        LOG.debug('Runner do_run result', extra=extra)

        extra = {'liveaction_db': liveaction_db}
        LOG.audit('Liveaction completed', extra=extra)

        return liveaction_db.result
Exemple #5
0
    def _do_enforce(self):
        # TODO: Refactor this to avoid additional lookup in cast_params
        action_ref = self.rule.action["ref"]

        # Verify action referenced in the rule exists in the database
        action_db = action_utils.get_action_by_ref(action_ref)
        if not action_db:
            raise ValueError('Action "%s" doesn\'t exist' % (action_ref))

        runnertype_db = action_utils.get_runnertype_by_name(
            action_db.runner_type["name"]
        )

        params = self.rule.action.parameters
        LOG.info(
            "Invoking action %s for trigger_instance %s with params %s.",
            self.rule.action.ref,
            self.trigger_instance.id,
            json.dumps(params),
        )

        # update trace before invoking the action.
        trace_context = self._update_trace()
        LOG.debug("Updated trace %s with rule %s.", trace_context, self.rule.id)

        context, additional_contexts = self.get_action_execution_context(
            action_db=action_db, trace_context=trace_context
        )

        return self._invoke_action(
            action_db=action_db,
            runnertype_db=runnertype_db,
            params=params,
            context=context,
            additional_contexts=additional_contexts,
        )
Exemple #6
0
    def _register_action(self, pack, action):
        content = self._meta_loader.load(action)
        pack_field = content.get('pack', None)
        if not pack_field:
            content['pack'] = pack
            pack_field = pack
        if pack_field != pack:
            raise Exception(
                'Model is in pack "%s" but field "pack" is different: %s' %
                (pack, pack_field))

        action_api = ActionAPI(**content)

        try:
            action_api.validate()
        except jsonschema.ValidationError as e:
            # We throw a more user-friendly exception on invalid parameter name
            msg = str(e)

            is_invalid_parameter_name = 'does not match any of the regexes: ' in msg

            if is_invalid_parameter_name:
                match = re.search(
                    '\'(.+?)\' does not match any of the regexes', msg)

                if match:
                    parameter_name = match.groups()[0]
                else:
                    parameter_name = 'unknown'

                new_msg = (
                    'Parameter name "%s" is invalid. Valid characters for parameter name '
                    'are [a-zA-Z0-0_].' % (parameter_name))
                new_msg += '\n\n' + msg
                raise jsonschema.ValidationError(new_msg)
            raise e

        action_validator.validate_action(action_api)
        model = ActionAPI.to_model(action_api)

        action_ref = ResourceReference.to_string_reference(
            pack=pack, name=str(content['name']))
        existing = action_utils.get_action_by_ref(action_ref)
        if not existing:
            LOG.debug('Action %s not found. Creating new one with: %s',
                      action_ref, content)
        else:
            LOG.debug('Action %s found. Will be updated from: %s to: %s',
                      action_ref, existing, model)
            model.id = existing.id

        try:
            model = Action.add_or_update(model)
            extra = {'action_db': model}
            LOG.audit('Action updated. Action %s from %s.',
                      model,
                      action,
                      extra=extra)
        except Exception:
            LOG.exception('Failed to write action to db %s.', model.name)
            raise
    def run(self, action_parameters):
        result = {'tasks': []}  # holds final result we store
        context_result = {
        }  # holds result which is used for the template context purposes
        top_level_error = None  # stores a reference to a top level error
        fail = True
        action_node = None

        try:
            action_node = self.chain_holder.get_next_node()
        except Exception as e:
            LOG.exception('Failed to get starting node "%s".',
                          action_node.name)

            error = ('Failed to get starting node "%s". Lookup failed: %s' %
                     (action_node.name, str(e)))
            trace = traceback.format_exc(10)
            top_level_error = {'error': error, 'traceback': trace}

        while action_node:
            fail = False
            error = None
            resolved_params = None
            liveaction = None

            created_at = datetime.datetime.now()

            try:
                resolved_params = ActionChainRunner._resolve_params(
                    action_node=action_node,
                    original_parameters=action_parameters,
                    results=context_result,
                    chain_vars=self.chain_holder.vars)
            except Exception as e:
                # Rendering parameters failed before we even got to running this action, abort and
                # fail the whole action chain
                LOG.exception('Failed to run action "%s".', action_node.name)

                fail = True
                error = (
                    'Failed to run task "%s". Parameter rendering failed: %s' %
                    (action_node.name, str(e)))
                trace = traceback.format_exc(10)
                top_level_error = {'error': error, 'traceback': trace}
                break

            # Verify that the referenced action exists
            # TODO: We do another lookup in cast_param, refactor to reduce number of lookups
            action_ref = action_node.ref
            action_db = action_db_util.get_action_by_ref(ref=action_ref)

            if not action_db:
                error = (
                    'Failed to run task "%s". Action with reference "%s" doesn\'t exist.'
                    % (action_node.name, action_ref))
                LOG.exception(error)

                fail = True
                top_level_error = {'error': error, 'traceback': error}
                break

            try:
                liveaction = ActionChainRunner._run_action(
                    action_node=action_node,
                    parent_execution_id=self.liveaction_id,
                    params=resolved_params)
            except Exception as e:
                # Save the traceback and error message
                LOG.exception('Failure in running action "%s".',
                              action_node.name)

                error = {
                    'error':
                    'Task "%s" failed: %s' % (action_node.name, str(e)),
                    'traceback': traceback.format_exc(10)
                }
                context_result[action_node.name] = error
            else:
                # Update context result
                context_result[action_node.name] = liveaction.result

                # Render and publish variables
                rendered_publish_vars = ActionChainRunner._render_publish_vars(
                    action_node=action_node,
                    action_parameters=action_parameters,
                    execution_result=liveaction.result,
                    previous_execution_results=context_result,
                    chain_vars=self.chain_holder.vars)

                if rendered_publish_vars:
                    self.chain_holder.vars.update(rendered_publish_vars)
            finally:
                # Record result and resolve a next node based on the task success or failure
                updated_at = datetime.datetime.now()

                format_kwargs = {
                    'action_node': action_node,
                    'liveaction_db': liveaction,
                    'created_at': created_at,
                    'updated_at': updated_at
                }

                if error:
                    format_kwargs['error'] = error

                task_result = self._format_action_exec_result(**format_kwargs)
                result['tasks'].append(task_result)

                try:
                    if not liveaction or liveaction.status == LIVEACTION_STATUS_FAILED:
                        fail = True
                        action_node = self.chain_holder.get_next_node(
                            action_node.name, condition='on-failure')
                    elif liveaction.status == LIVEACTION_STATUS_SUCCEEDED:
                        action_node = self.chain_holder.get_next_node(
                            action_node.name, condition='on-success')
                except Exception as e:
                    LOG.exception('Failed to get next node "%s".',
                                  action_node.name)

                    fail = True
                    error = (
                        'Failed to get next node "%s". Lookup failed: %s' %
                        (action_node.name, str(e)))
                    trace = traceback.format_exc(10)
                    top_level_error = {'error': error, 'traceback': trace}
                    # reset action_node here so that chain breaks on failure.
                    action_node = None

        if fail:
            status = LIVEACTION_STATUS_FAILED
        else:
            status = LIVEACTION_STATUS_SUCCEEDED

        if top_level_error:
            # Include top level error information
            result['error'] = top_level_error['error']
            result['traceback'] = top_level_error['traceback']

        return (status, result, None)
Exemple #8
0
def inspect_task_contents(wf_spec):
    result = []
    spec_path = 'tasks'
    schema_path = 'properties.tasks.patternProperties.^\\w+$'
    action_schema_path = schema_path + '.properties.action'
    action_input_schema_path = schema_path + '.properties.input'

    def is_action_an_expression(action):
        if isinstance(action, six.string_types):
            for name, evaluator in six.iteritems(expressions.get_evaluators()):
                if evaluator.has_expressions(action):
                    return True

    for task_name, task_spec in six.iteritems(wf_spec.tasks):
        action_ref = getattr(task_spec, 'action', None)
        action_spec_path = spec_path + '.' + task_name + '.action'
        action_input_spec_path = spec_path + '.' + task_name + '.input'

        # Move on if action is empty or an expression.
        if not action_ref or is_action_an_expression(action_ref):
            continue

        # Check that the format of the action is a valid resource reference.
        if not sys_models.ResourceReference.is_resource_reference(action_ref):
            entry = {
                'type':
                'content',
                'message':
                'The action reference "%s" is not formatted correctly.' %
                action_ref,
                'spec_path':
                action_spec_path,
                'schema_path':
                action_schema_path
            }

            result.append(entry)
            continue

        # Check that the action is registered in the database.
        if not action_utils.get_action_by_ref(ref=action_ref):
            entry = {
                'type': 'content',
                'message':
                'The action "%s" is not registered in the database.' %
                action_ref,
                'spec_path': action_spec_path,
                'schema_path': action_schema_path
            }

            result.append(entry)
            continue

        # Check the action parameters.
        params = getattr(task_spec, 'input', None) or {}

        if params and not isinstance(params, dict):
            continue

        requires, unexpected = action_param_utils.validate_action_parameters(
            action_ref, params)

        for param in requires:
            message = 'Action "%s" is missing required input "%s".' % (
                action_ref, param)

            entry = {
                'type': 'content',
                'message': message,
                'spec_path': action_input_spec_path,
                'schema_path': action_input_schema_path
            }

            result.append(entry)

        for param in unexpected:
            message = 'Action "%s" has unexpected input "%s".' % (action_ref,
                                                                  param)

            entry = {
                'type':
                'content',
                'message':
                message,
                'spec_path':
                action_input_spec_path + '.' + param,
                'schema_path':
                action_input_schema_path + '.patternProperties.^\\w+$'
            }

            result.append(entry)

    return result
Exemple #9
0
def request(wf_def, ac_ex_db, st2_ctx):
    wf_ac_ex_id = str(ac_ex_db.id)
    LOG.info('[%s] Processing action execution request for workflow.',
             wf_ac_ex_id)

    # Load workflow definition into workflow spec model.
    spec_module = specs_loader.get_spec_module('native')
    wf_spec = spec_module.instantiate(wf_def)

    # Inspect the workflow spec.
    inspect(wf_spec, st2_ctx, raise_exception=True)

    # Identify the action to execute.
    action_db = action_utils.get_action_by_ref(ref=ac_ex_db.action['ref'])

    if not action_db:
        error = 'Unable to find action "%s".' % ac_ex_db.action['ref']
        raise ac_exc.InvalidActionReferencedException(error)

    # Identify the runner for the action.
    runner_type_db = action_utils.get_runnertype_by_name(
        action_db.runner_type['name'])

    # Render action execution parameters.
    runner_params, action_params = param_utils.render_final_params(
        runner_type_db.runner_parameters, action_db.parameters,
        ac_ex_db.parameters, ac_ex_db.context)

    # Instantiate the workflow conductor.
    conductor_params = {'inputs': action_params, 'context': st2_ctx}
    conductor = conducting.WorkflowConductor(wf_spec, **conductor_params)

    # Set the initial workflow state to requested.
    conductor.request_workflow_state(states.REQUESTED)

    # Serialize the conductor which initializes some internal values.
    data = conductor.serialize()

    # Create a record for workflow execution.
    wf_ex_db = wf_db_models.WorkflowExecutionDB(action_execution=str(
        ac_ex_db.id),
                                                spec=data['spec'],
                                                graph=data['graph'],
                                                flow=data['flow'],
                                                context=data['context'],
                                                input=data['input'],
                                                output=data['output'],
                                                errors=data['errors'],
                                                status=data['state'])

    # Insert new record into the database and do not publish to the message bus yet.
    wf_ex_db = wf_db_access.WorkflowExecution.insert(wf_ex_db, publish=False)
    LOG.info('[%s] Workflow execution "%s" is created.', wf_ac_ex_id,
             str(wf_ex_db.id))

    # Update the context with the workflow execution id created on database insert.
    # Publish the workflow execution requested state to the message bus.
    if wf_ex_db.status not in states.COMPLETED_STATES:
        wf_ex_db.context['st2']['workflow_execution_id'] = str(wf_ex_db.id)
        wf_ex_db.flow['contexts'][0]['value']['st2'][
            'workflow_execution_id'] = str(wf_ex_db.id)
        wf_ex_db = wf_db_access.WorkflowExecution.update(wf_ex_db,
                                                         publish=False)
        wf_db_access.WorkflowExecution.publish_status(wf_ex_db)
        msg = '[%s] Workflow execution "%s" is published.'
        LOG.info(msg, wf_ac_ex_id, str(wf_ex_db.id))
    else:
        msg = '[%s] Workflow execution is in completed state "%s".'
        LOG.info(msg, wf_ac_ex_id, wf_ex_db.status)

    return wf_ex_db
Exemple #10
0
    def _handle_schedule_execution(self,
                                   liveaction_api,
                                   requester_user,
                                   context_string=None,
                                   show_secrets=False):
        """
        :param liveaction: LiveActionAPI object.
        :type liveaction: :class:`LiveActionAPI`
        """
        if not requester_user:
            requester_user = UserDB(name=cfg.CONF.system_user.user)

        # Assert action ref is valid
        action_ref = liveaction_api.action
        action_db = action_utils.get_action_by_ref(action_ref)

        if not action_db:
            message = 'Action "%s" cannot be found.' % (action_ref)
            LOG.warning(message)
            abort(http_client.BAD_REQUEST, message)

        # Assert the permissions
        permission_type = PermissionType.ACTION_EXECUTE
        rbac_utils = get_rbac_backend().get_utils_class()
        rbac_utils.assert_user_has_resource_db_permission(
            user_db=requester_user,
            resource_db=action_db,
            permission_type=permission_type,
        )

        # Validate that the authenticated user is admin if user query param is provided
        user = liveaction_api.user or requester_user.name
        rbac_utils.assert_user_is_admin_if_user_query_param_is_provided(
            user_db=requester_user, user=user)

        try:
            return self._schedule_execution(
                liveaction=liveaction_api,
                requester_user=requester_user,
                user=user,
                context_string=context_string,
                show_secrets=show_secrets,
                action_db=action_db,
            )
        except ValueError as e:
            LOG.exception("Unable to execute action.")
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except jsonschema.ValidationError as e:
            LOG.exception(
                "Unable to execute action. Parameter validation failed.")
            abort(
                http_client.BAD_REQUEST,
                re.sub("u'([^']*)'", r"'\1'",
                       getattr(e, "message", six.text_type(e))),
            )
        except trace_exc.TraceNotFoundException as e:
            abort(http_client.BAD_REQUEST, six.text_type(e))
        except validation_exc.ValueValidationException as e:
            raise e
        except Exception as e:
            LOG.exception(
                "Unable to execute action. Unexpected error encountered.")
            abort(http_client.INTERNAL_SERVER_ERROR, six.text_type(e))
Exemple #11
0
    def test_triggered_execution(self):
        docs = {
            'trigger_type':
            copy.deepcopy(fixture.ARTIFACTS['trigger_type']),
            'trigger':
            copy.deepcopy(fixture.ARTIFACTS['trigger']),
            'rule':
            copy.deepcopy(fixture.ARTIFACTS['rule']),
            'trigger_instance':
            copy.deepcopy(fixture.ARTIFACTS['trigger_instance'])
        }

        # Trigger an action execution.
        trigger_type = TriggerType.add_or_update(
            TriggerTypeAPI.to_model(TriggerTypeAPI(**docs['trigger_type'])))
        trigger = Trigger.add_or_update(
            TriggerAPI.to_model(TriggerAPI(**docs['trigger'])))
        rule = RuleAPI.to_model(RuleAPI(**docs['rule']))
        rule.trigger = reference.get_str_resource_ref_from_model(trigger)
        rule = Rule.add_or_update(rule)
        trigger_instance = TriggerInstance.add_or_update(
            TriggerInstanceAPI.to_model(
                TriggerInstanceAPI(**docs['trigger_instance'])))
        trace_service.add_or_update_given_trace_context(
            trace_context={'trace_tag': 'test_triggered_execution_trace'},
            trigger_instances=[str(trigger_instance.id)])
        enforcer = RuleEnforcer(trigger_instance, rule)
        enforcer.enforce()

        # Wait for the action execution to complete and then confirm outcome.
        liveaction = LiveAction.get(
            context__trigger_instance__id=str(trigger_instance.id))
        self.assertIsNotNone(liveaction)
        liveaction = LiveAction.get_by_id(str(liveaction.id))
        self.assertEqual(liveaction.status,
                         action_constants.LIVEACTION_STATUS_FAILED)
        execution = self._get_action_execution(liveaction__id=str(
            liveaction.id),
                                               raise_exception=True)
        self.assertDictEqual(execution.trigger,
                             vars(TriggerAPI.from_model(trigger)))
        self.assertDictEqual(execution.trigger_type,
                             vars(TriggerTypeAPI.from_model(trigger_type)))
        self.assertDictEqual(
            execution.trigger_instance,
            vars(TriggerInstanceAPI.from_model(trigger_instance)))
        self.assertDictEqual(execution.rule, vars(RuleAPI.from_model(rule)))
        action = action_utils.get_action_by_ref(liveaction.action)
        self.assertDictEqual(execution.action,
                             vars(ActionAPI.from_model(action)))
        runner = RunnerType.get_by_name(action.runner_type['name'])
        self.assertDictEqual(execution.runner,
                             vars(RunnerTypeAPI.from_model(runner)))
        liveaction = LiveAction.get_by_id(str(liveaction.id))
        self.assertEqual(execution.start_timestamp, liveaction.start_timestamp)
        self.assertEqual(execution.end_timestamp, liveaction.end_timestamp)
        self.assertEqual(execution.result, liveaction.result)
        self.assertEqual(execution.status, liveaction.status)
        self.assertEqual(execution.context, liveaction.context)
        self.assertEqual(execution.liveaction['callback'], liveaction.callback)
        self.assertEqual(execution.liveaction['action'], liveaction.action)
Exemple #12
0
def _transform_action(name, spec):

    action_key, input_key = None, None

    for spec_type, spec_meta in six.iteritems(SPEC_TYPES):
        if spec_meta['action_key'] in spec:
            action_key = spec_meta['action_key']
            input_key = spec_meta['input_key']
            break

    if not action_key:
        return

    if spec[action_key] == 'st2.callback':
        raise WorkflowDefinitionException('st2.callback is deprecated.')

    # Convert parameters that are inline (i.e. action: some_action var1={$.value1} var2={$.value2})
    # and split it to action name and input dict as illustrated below.
    #
    # action: some_action
    # input:
    #   var1: <% $.value1 %>
    #   var2: <% $.value2 %>
    #
    # This step to separate the action name and the input parameters is required
    # to wrap them with the st2.action proxy.
    #
    # action: st2.action
    # input:
    #   ref: some_action
    #   parameters:
    #     var1: <% $.value1 %>
    #     var2: <% $.value2 %>
    _eval_inline_params(spec, action_key, input_key)

    transformed = (spec[action_key] == 'st2.action')

    action_ref = spec[input_key]['ref'] if transformed else spec[action_key]

    action = None

    # Identify if action is a registered StackStorm action.
    if action_ref and ResourceReference.is_resource_reference(action_ref):
        action = action_utils.get_action_by_ref(ref=action_ref)

    # If action is a registered StackStorm action, then wrap the
    # action with the st2 proxy and validate the action input.
    if action:
        if not transformed:
            spec[action_key] = 'st2.action'
            action_input = spec.get(input_key)
            spec[input_key] = {'ref': action_ref}
            if action_input:
                spec[input_key]['parameters'] = action_input

        action_input = spec.get(input_key, {})
        action_params = action_input.get('parameters', {})
        _validate_action_parameters(name, action, action_params)

        xformed_action_params = {}

        for param_name in action_params.keys():
            param_value = copy.deepcopy(action_params[param_name])
            xformed_param_value = _transform_action_param(
                action_ref, param_name, param_value)
            xformed_action_params[param_name] = xformed_param_value

        if xformed_action_params != action_params:
            spec[input_key]['parameters'] = xformed_action_params
Exemple #13
0
    def test_triggered_execution(self):
        docs = {
            "trigger_type":
            copy.deepcopy(fixture.ARTIFACTS["trigger_type"]),
            "trigger":
            copy.deepcopy(fixture.ARTIFACTS["trigger"]),
            "rule":
            copy.deepcopy(fixture.ARTIFACTS["rule"]),
            "trigger_instance":
            copy.deepcopy(fixture.ARTIFACTS["trigger_instance"]),
        }

        # Trigger an action execution.
        trigger_type = TriggerType.add_or_update(
            TriggerTypeAPI.to_model(TriggerTypeAPI(**docs["trigger_type"])))
        trigger = Trigger.add_or_update(
            TriggerAPI.to_model(TriggerAPI(**docs["trigger"])))
        rule = RuleAPI.to_model(RuleAPI(**docs["rule"]))
        rule.trigger = reference.get_str_resource_ref_from_model(trigger)
        rule = Rule.add_or_update(rule)
        trigger_instance = TriggerInstance.add_or_update(
            TriggerInstanceAPI.to_model(
                TriggerInstanceAPI(**docs["trigger_instance"])))
        trace_service.add_or_update_given_trace_context(
            trace_context={"trace_tag": "test_triggered_execution_trace"},
            trigger_instances=[str(trigger_instance.id)],
        )
        enforcer = RuleEnforcer(trigger_instance, rule)
        enforcer.enforce()

        # Wait for the action execution to complete and then confirm outcome.
        liveaction = LiveAction.get(
            context__trigger_instance__id=str(trigger_instance.id))
        self.assertIsNotNone(liveaction)
        liveaction = self._wait_on_status(
            liveaction, action_constants.LIVEACTION_STATUS_FAILED)

        execution = self._get_action_execution(liveaction__id=str(
            liveaction.id),
                                               raise_exception=True)

        self.assertDictEqual(execution.trigger,
                             vars(TriggerAPI.from_model(trigger)))
        self.assertDictEqual(execution.trigger_type,
                             vars(TriggerTypeAPI.from_model(trigger_type)))
        self.assertDictEqual(
            execution.trigger_instance,
            vars(TriggerInstanceAPI.from_model(trigger_instance)),
        )
        self.assertDictEqual(execution.rule, vars(RuleAPI.from_model(rule)))
        action = action_utils.get_action_by_ref(liveaction.action)
        self.assertDictEqual(execution.action,
                             vars(ActionAPI.from_model(action)))
        runner = RunnerType.get_by_name(action.runner_type["name"])
        self.assertDictEqual(execution.runner,
                             vars(RunnerTypeAPI.from_model(runner)))
        liveaction = LiveAction.get_by_id(str(liveaction.id))
        self.assertEqual(execution.start_timestamp, liveaction.start_timestamp)
        # NOTE: Timestamp of liveaction and execution may be a bit different, depending on how long
        # it takes to persist each object in the database
        self.assertEqual(
            execution.end_timestamp.replace(microsecond=0),
            liveaction.end_timestamp.replace(microsecond=0),
        )
        self.assertEqual(execution.result, liveaction.result)
        self.assertEqual(execution.status, liveaction.status)
        self.assertEqual(execution.context, liveaction.context)
        self.assertEqual(execution.liveaction["callback"], liveaction.callback)
        self.assertEqual(execution.liveaction["action"], liveaction.action)
Exemple #14
0
    def _register_action(self, pack, action):
        content = self._meta_loader.load(action)
        pack_field = content.get('pack', None)
        if not pack_field:
            content['pack'] = pack
            pack_field = pack
        if pack_field != pack:
            raise Exception(
                'Model is in pack "%s" but field "pack" is different: %s' %
                (pack, pack_field))

        # Add in "metadata_file" attribute which stores path to the pack metadata file relative to
        # the pack directory
        metadata_file = content_utils.get_relative_path_to_pack_file(
            pack_ref=pack, file_path=action, use_pack_cache=True)
        content['metadata_file'] = metadata_file

        action_api = ActionAPI(**content)

        try:
            action_api.validate()
        except jsonschema.ValidationError as e:
            # We throw a more user-friendly exception on invalid parameter name
            msg = six.text_type(e)

            is_invalid_parameter_name = 'does not match any of the regexes: ' in msg

            if is_invalid_parameter_name:
                match = re.search(
                    '\'(.+?)\' does not match any of the regexes', msg)

                if match:
                    parameter_name = match.groups()[0]
                else:
                    parameter_name = 'unknown'

                new_msg = (
                    'Parameter name "%s" is invalid. Valid characters for parameter name '
                    'are [a-zA-Z0-0_].' % (parameter_name))
                new_msg += '\n\n' + msg
                raise jsonschema.ValidationError(new_msg)
            raise e

        # Use in-memory cached RunnerTypeDB objects to reduce load on the database
        if self._use_runners_cache:
            runner_type_db = self._runner_type_db_cache.get(
                action_api.runner_type, None)

            if not runner_type_db:
                runner_type_db = action_validator.get_runner_model(action_api)
                self._runner_type_db_cache[
                    action_api.runner_type] = runner_type_db
        else:
            runner_type_db = None

        action_validator.validate_action(action_api,
                                         runner_type_db=runner_type_db)
        model = ActionAPI.to_model(action_api)

        action_ref = ResourceReference.to_string_reference(
            pack=pack, name=str(content['name']))
        existing = action_utils.get_action_by_ref(action_ref)
        if not existing:
            LOG.debug('Action %s not found. Creating new one with: %s',
                      action_ref, content)
        else:
            LOG.debug('Action %s found. Will be updated from: %s to: %s',
                      action_ref, existing, model)
            model.id = existing.id

        try:
            model = Action.add_or_update(model)
            extra = {'action_db': model}
            LOG.audit('Action updated. Action %s from %s.',
                      model,
                      action,
                      extra=extra)
        except Exception:
            LOG.exception('Failed to write action to db %s.', model.name)
            raise
Exemple #15
0
    def _schedule_execution(self,
                            liveaction,
                            requester_user,
                            user=None,
                            context_string=None,
                            show_secrets=False):
        # Initialize execution context if it does not exist.
        if not hasattr(liveaction, 'context'):
            liveaction.context = dict()

        liveaction.context['user'] = user
        LOG.debug('User is: %s' % liveaction.context['user'])

        # Retrieve other st2 context from request header.
        if context_string:
            context = try_loads(context_string)
            if not isinstance(context, dict):
                raise ValueError(
                    'Unable to convert st2-context from the headers into JSON.'
                )
            liveaction.context.update(context)

        # Schedule the action execution.
        liveaction_db = LiveActionAPI.to_model(liveaction)
        liveaction_db, actionexecution_db = action_service.create_request(
            liveaction_db)

        action_db = action_utils.get_action_by_ref(liveaction_db.action)
        runnertype_db = action_utils.get_runnertype_by_name(
            action_db.runner_type['name'])

        try:
            liveaction_db.parameters = param_utils.render_live_params(
                runnertype_db.runner_parameters, action_db.parameters,
                liveaction_db.parameters, liveaction_db.context)
        except ParamException:
            # By this point the execution is already in the DB therefore need to mark it failed.
            _, e, tb = sys.exc_info()
            action_service.update_status(liveaction=liveaction_db,
                                         new_status=LIVEACTION_STATUS_FAILED,
                                         result={
                                             'error':
                                             str(e),
                                             'traceback':
                                             ''.join(
                                                 traceback.format_tb(tb, 20))
                                         })
            # Might be a good idea to return the actual ActionExecution rather than bubble up
            # the execption.
            raise ValueValidationException(str(e))

        liveaction_db = LiveAction.add_or_update(liveaction_db, publish=False)

        _, actionexecution_db = action_service.publish_request(
            liveaction_db, actionexecution_db)
        mask_secrets = self._get_mask_secrets(requester_user,
                                              show_secrets=show_secrets)
        execution_api = ActionExecutionAPI.from_model(
            actionexecution_db, mask_secrets=mask_secrets)

        return Response(json=execution_api, status=http_client.CREATED)
Exemple #16
0
def request(wf_def, ac_ex_db, st2_ctx, notify_cfg=None):
    wf_ac_ex_id = str(ac_ex_db.id)
    LOG.info('[%s] Processing action execution request for workflow.',
             wf_ac_ex_id)

    # Load workflow definition into workflow spec model.
    spec_module = specs_loader.get_spec_module('native')
    wf_spec = spec_module.instantiate(wf_def)

    # Inspect the workflow spec.
    inspect(wf_spec, st2_ctx, raise_exception=True)

    # Identify the action to execute.
    action_db = action_utils.get_action_by_ref(ref=ac_ex_db.action['ref'])

    if not action_db:
        error = 'Unable to find action "%s".' % ac_ex_db.action['ref']
        raise ac_exc.InvalidActionReferencedException(error)

    # Identify the runner for the action.
    runner_type_db = action_utils.get_runnertype_by_name(
        action_db.runner_type['name'])

    # Render action execution parameters.
    runner_params, action_params = param_utils.render_final_params(
        runner_type_db.runner_parameters, action_db.parameters,
        ac_ex_db.parameters, ac_ex_db.context)

    # Instantiate the workflow conductor.
    conductor_params = {'inputs': action_params, 'context': st2_ctx}
    conductor = conducting.WorkflowConductor(wf_spec, **conductor_params)

    # Serialize the conductor which initializes some internal values.
    data = conductor.serialize()

    # Create a record for workflow execution.
    wf_ex_db = wf_db_models.WorkflowExecutionDB(action_execution=str(
        ac_ex_db.id),
                                                spec=data['spec'],
                                                graph=data['graph'],
                                                input=data['input'],
                                                context=data['context'],
                                                state=data['state'],
                                                status=data['state']['status'],
                                                output=data['output'],
                                                errors=data['errors'])

    # Inspect that the list of tasks in the notify parameter exist in the workflow spec.
    if runner_params.get('notify'):
        invalid_tasks = list(
            set(runner_params.get('notify')) - set(wf_spec.tasks.keys()))

        if invalid_tasks:
            raise wf_exc.WorkflowExecutionException(
                'The following tasks in the notify parameter do not exist '
                'in the workflow definition: %s.' % ', '.join(invalid_tasks))

    # Write notify instruction to record.
    if notify_cfg:
        # Set up the notify instruction in the workflow execution record.
        wf_ex_db.notify = {
            'config': notify_cfg,
            'tasks': runner_params.get('notify')
        }

    # Insert new record into the database and do not publish to the message bus yet.
    wf_ex_db = wf_db_access.WorkflowExecution.insert(wf_ex_db, publish=False)
    LOG.info('[%s] Workflow execution "%s" is created.', wf_ac_ex_id,
             str(wf_ex_db.id))

    # Update the context with the workflow execution id created on database insert.
    # Publish the workflow execution requested status to the message bus.
    if wf_ex_db.status not in statuses.COMPLETED_STATUSES:
        # Set the initial workflow status to requested.
        conductor.request_workflow_status(statuses.REQUESTED)
        data = conductor.serialize()
        wf_ex_db.state = data['state']
        wf_ex_db.status = data['state']['status']

        # Put the ID of the workflow execution record in the context.
        wf_ex_db.context['st2']['workflow_execution_id'] = str(wf_ex_db.id)
        wf_ex_db.state['contexts'][0]['st2']['workflow_execution_id'] = str(
            wf_ex_db.id)

        # Update the workflow execution record.
        wf_ex_db = wf_db_access.WorkflowExecution.update(wf_ex_db,
                                                         publish=False)
        wf_db_access.WorkflowExecution.publish_status(wf_ex_db)
        msg = '[%s] Workflow execution "%s" is published.'
        LOG.info(msg, wf_ac_ex_id, str(wf_ex_db.id))
    else:
        msg = '[%s] Unable to request workflow execution. It is already in completed status "%s".'
        LOG.info(msg, wf_ac_ex_id, wf_ex_db.status)

    return wf_ex_db
Exemple #17
0
def request_action_execution(wf_ex_db,
                             task_ex_db,
                             st2_ctx,
                             ac_ex_req,
                             delay=None):
    wf_ac_ex_id = wf_ex_db.action_execution
    action_ref = ac_ex_req['action']
    action_input = ac_ex_req['input']
    item_id = ac_ex_req.get('item_id')

    # If the task is with items and item_id is not provided, raise exception.
    if task_ex_db.itemized and item_id is None:
        msg = 'Unable to request action execution. Identifier for the item is not provided.'
        raise Exception(msg)

    # Identify the action to execute.
    action_db = action_utils.get_action_by_ref(ref=action_ref)

    if not action_db:
        error = 'Unable to find action "%s".' % action_ref
        raise ac_exc.InvalidActionReferencedException(error)

    # Identify the runner for the action.
    runner_type_db = action_utils.get_runnertype_by_name(
        action_db.runner_type['name'])

    # Identify action pack name
    pack_name = action_ref.split('.')[0] if action_ref else st2_ctx.get('pack')

    # Set context for the action execution.
    ac_ex_ctx = {
        'pack': pack_name,
        'user': st2_ctx.get('user'),
        'parent': st2_ctx,
        'orquesta': {
            'workflow_execution_id': str(wf_ex_db.id),
            'task_execution_id': str(task_ex_db.id),
            'task_name': task_ex_db.task_name,
            'task_id': task_ex_db.task_id,
            'task_route': task_ex_db.task_route
        }
    }

    if st2_ctx.get('api_user'):
        ac_ex_ctx['api_user'] = st2_ctx.get('api_user')

    if st2_ctx.get('source_channel'):
        ac_ex_ctx['source_channel'] = st2_ctx.get('source_channel')

    if item_id is not None:
        ac_ex_ctx['orquesta']['item_id'] = item_id

    # Render action execution parameters and setup action execution object.
    ac_ex_params = param_utils.render_live_params(
        runner_type_db.runner_parameters or {}, action_db.parameters or {},
        action_input or {}, ac_ex_ctx)

    # The delay spec is in seconds and scheduler expects milliseconds.
    if delay is not None and delay > 0:
        delay = delay * 1000

    # Instantiate the live action record.
    lv_ac_db = lv_db_models.LiveActionDB(action=action_ref,
                                         workflow_execution=str(wf_ex_db.id),
                                         task_execution=str(task_ex_db.id),
                                         delay=delay,
                                         context=ac_ex_ctx,
                                         parameters=ac_ex_params)

    # Set notification if instructed.
    if (wf_ex_db.notify and wf_ex_db.notify.get('config')
            and wf_ex_db.notify.get('tasks')
            and task_ex_db.task_name in wf_ex_db.notify['tasks']):
        lv_ac_db.notify = notify_api_models.NotificationsHelper.to_model(
            wf_ex_db.notify['config'])

    # Set the task execution to running first otherwise a race can occur
    # where the action execution finishes first and the completion handler
    # conflicts with this status update.
    task_ex_db.status = statuses.RUNNING
    task_ex_db = wf_db_access.TaskExecution.update(task_ex_db, publish=False)

    # Request action execution.
    lv_ac_db, ac_ex_db = ac_svc.request(lv_ac_db)
    msg = '[%s] Action execution "%s" requested for task "%s", route "%s".'
    LOG.info(msg, wf_ac_ex_id, str(ac_ex_db.id), task_ex_db.task_id,
             str(task_ex_db.task_route))

    return ac_ex_db
Exemple #18
0
def purge_inquiries(logger):
    """Purge Inquiries that have exceeded their configured TTL

    At the moment, Inquiries do not have their own database model, so this function effectively
    is another, more specialized GC for executions. It will look for executions with a 'pending'
    status that use the 'inquirer' runner, which is the current definition for an Inquiry.

    Then it will mark those that have a nonzero TTL have existed longer than their TTL as
    "timed out". It will then request that the parent workflow(s) resume, where the failure
    can be handled as the user desires.
    """

    # Get all existing Inquiries
    filters = {
        'runner__name': 'inquirer',
        'status': action_constants.LIVEACTION_STATUS_PENDING
    }
    inquiries = list(ActionExecution.query(**filters))

    gc_count = 0

    # Inspect each Inquiry, and determine if TTL is expired
    for inquiry in inquiries:

        ttl = int(inquiry.result.get('ttl'))
        if ttl <= 0:
            logger.debug("Inquiry %s has a TTL of %s. Skipping." %
                         (inquiry.id, ttl))
            continue

        min_since_creation = int(
            (get_datetime_utc_now() - inquiry.start_timestamp).total_seconds()
            / 60)

        logger.debug(
            "Inquiry %s has a TTL of %s and was started %s minute(s) ago" %
            (inquiry.id, ttl, min_since_creation))

        if min_since_creation > ttl:
            gc_count += 1
            logger.info("TTL expired for Inquiry %s. Marking as timed out." %
                        inquiry.id)

            liveaction_db = action_utils.update_liveaction_status(
                status=action_constants.LIVEACTION_STATUS_TIMED_OUT,
                result=inquiry.result,
                liveaction_id=inquiry.liveaction.get('id'))
            executions.update_execution(liveaction_db)

            # Call Inquiry runner's post_run to trigger callback to workflow
            action_db = get_action_by_ref(liveaction_db.action)
            invoke_post_run(liveaction_db=liveaction_db, action_db=action_db)

            if liveaction_db.context.get("parent"):
                # Request that root workflow resumes
                root_liveaction = action_service.get_root_liveaction(
                    liveaction_db)
                action_service.request_resume(
                    root_liveaction, UserDB(cfg.CONF.system_user.user))

    logger.info('Marked %s ttl-expired Inquiries as "timed out".' % (gc_count))
Exemple #19
0
    def _register_action(self, pack, action):
        content = self._meta_loader.load(action)
        pack_field = content.get("pack", None)
        if not pack_field:
            content["pack"] = pack
            pack_field = pack
        if pack_field != pack:
            raise Exception(
                'Model is in pack "%s" but field "pack" is different: %s' %
                (pack, pack_field))

        # Add in "metadata_file" attribute which stores path to the pack metadata file relative to
        # the pack directory
        metadata_file = content_utils.get_relative_path_to_pack_file(
            pack_ref=pack, file_path=action, use_pack_cache=True)
        content["metadata_file"] = metadata_file

        action_api = ActionAPI(**content)

        try:
            action_api.validate()
        except jsonschema.ValidationError as e:
            # We throw a more user-friendly exception on invalid parameter name
            msg = six.text_type(e)

            is_invalid_parameter_name = "does not match any of the regexes: " in msg

            if is_invalid_parameter_name:
                match = re.search("'(.+?)' does not match any of the regexes",
                                  msg)

                if match:
                    parameter_name = match.groups()[0]
                else:
                    parameter_name = "unknown"

                new_msg = (
                    'Parameter name "%s" is invalid. Valid characters for parameter name '
                    "are [a-zA-Z0-0_]." % (parameter_name))
                new_msg += "\n\n" + msg
                raise jsonschema.ValidationError(new_msg)
            raise e

        # Use in-memory cached RunnerTypeDB objects to reduce load on the database
        if self._use_runners_cache:
            runner_type_db = self._runner_type_db_cache.get(
                action_api.runner_type, None)

            if not runner_type_db:
                runner_type_db = action_validator.get_runner_model(action_api)
                self._runner_type_db_cache[
                    action_api.runner_type] = runner_type_db
        else:
            runner_type_db = None

        action_validator.validate_action(action_api,
                                         runner_type_db=runner_type_db)
        model = ActionAPI.to_model(action_api)

        action_ref = ResourceReference.to_string_reference(
            pack=pack, name=str(content["name"]))
        # NOTE: Here we only retrieve existing object to perform an upsert if it already exists in
        # the database. To do that, we only need access to the "id" attribute (and ref and pack
        # for our ActionDB abstraction). Retrieving only those fields is fast and much efficient
        # especially for actions like aws pack ones which contain a lot of parameters.
        existing = action_utils.get_action_by_ref(
            action_ref, only_fields=["id", "ref", "pack"])
        if not existing:
            LOG.debug("Action %s not found. Creating new one with: %s",
                      action_ref, content)
        else:
            LOG.debug(
                "Action %s found. Will be updated from: %s to: %s",
                action_ref,
                existing,
                model,
            )
            model.id = existing.id

        try:
            model = Action.add_or_update(model)
            extra = {"action_db": model}
            LOG.audit("Action updated. Action %s from %s.",
                      model,
                      action,
                      extra=extra)
        except Exception:
            LOG.exception("Failed to write action to db %s.", model.name)
            raise
Exemple #20
0
def request(liveaction):
    """
    Request an action execution.

    :return: (liveaction, execution)
    :rtype: tuple
    """
    # Use the user context from the parent action execution. Subtasks in a workflow
    # action can be invoked by a system user and so we want to use the user context
    # from the original workflow action.
    if getattr(liveaction, 'context', None) and 'parent' in liveaction.context:
        parent = LiveAction.get_by_id(liveaction.context['parent'])
        liveaction.context['user'] = getattr(parent, 'context',
                                             dict()).get('user')

    # Validate action.
    action_db = action_utils.get_action_by_ref(liveaction.action)
    if not action_db:
        raise ValueError('Action "%s" cannot be found.' % liveaction.action)
    if not action_db.enabled:
        raise ValueError('Unable to execute. Action "%s" is disabled.' %
                         liveaction.action)

    runnertype_db = action_utils.get_runnertype_by_name(
        action_db.runner_type['name'])

    if not hasattr(liveaction, 'parameters'):
        liveaction.parameters = dict()

    # Validate action parameters.
    schema = util_schema.get_parameter_schema(action_db)
    validator = util_schema.get_validator()
    util_schema.validate(liveaction.parameters,
                         schema,
                         validator,
                         use_default=True)

    # validate that no immutable params are being overriden. Although possible to
    # ignore the override it is safer to inform the user to avoid surprises.
    immutables = _get_immutable_params(action_db.parameters)
    immutables.extend(_get_immutable_params(runnertype_db.runner_parameters))
    overridden_immutables = [
        p for p in six.iterkeys(liveaction.parameters) if p in immutables
    ]
    if len(overridden_immutables) > 0:
        raise ValueError(
            'Override of immutable parameter(s) %s is unsupported.' %
            str(overridden_immutables))

    # Set notification settings for action.
    # XXX: There are cases when we don't want notifications to be sent for a particular
    # execution. So we should look at liveaction.parameters['notify']
    # and not set liveaction.notify.
    if action_db.notify:
        liveaction.notify = action_db.notify

    # Write to database and send to message queue.
    liveaction.status = action_constants.LIVEACTION_STATUS_REQUESTED
    liveaction.start_timestamp = date_utils.get_datetime_utc_now()

    # Publish creation after both liveaction and actionexecution are created.
    liveaction = LiveAction.add_or_update(liveaction, publish=False)
    execution = executions.create_execution_object(liveaction, publish=False)

    # Assume that this is a creation.
    LiveAction.publish_create(liveaction)
    LiveAction.publish_status(liveaction)
    ActionExecution.publish_create(execution)

    extra = {'liveaction_db': liveaction, 'execution_db': execution}
    LOG.audit(
        'Action execution requested. LiveAction.id=%s, ActionExecution.id=%s' %
        (liveaction.id, execution.id),
        extra=extra)

    return liveaction, execution
Exemple #21
0
def create_request(liveaction):
    """
    Create an action execution.

    :return: (liveaction, execution)
    :rtype: tuple
    """
    # Use the user context from the parent action execution. Subtasks in a workflow
    # action can be invoked by a system user and so we want to use the user context
    # from the original workflow action.
    parent_context = executions.get_parent_context(liveaction)
    if parent_context:
        parent_user = parent_context.get('user', None)
        if parent_user:
            liveaction.context['user'] = parent_user

    # Validate action.
    action_db = action_utils.get_action_by_ref(liveaction.action)
    if not action_db:
        raise ValueError('Action "%s" cannot be found.' % liveaction.action)
    if not action_db.enabled:
        raise ValueError('Unable to execute. Action "%s" is disabled.' %
                         liveaction.action)

    runnertype_db = action_utils.get_runnertype_by_name(
        action_db.runner_type['name'])

    if not hasattr(liveaction, 'parameters'):
        liveaction.parameters = dict()

    # Validate action parameters.
    schema = util_schema.get_schema_for_action_parameters(action_db)
    validator = util_schema.get_validator()
    util_schema.validate(liveaction.parameters,
                         schema,
                         validator,
                         use_default=True,
                         allow_default_none=True)

    # validate that no immutable params are being overriden. Although possible to
    # ignore the override it is safer to inform the user to avoid surprises.
    immutables = _get_immutable_params(action_db.parameters)
    immutables.extend(_get_immutable_params(runnertype_db.runner_parameters))
    overridden_immutables = [
        p for p in six.iterkeys(liveaction.parameters) if p in immutables
    ]
    if len(overridden_immutables) > 0:
        raise ValueError(
            'Override of immutable parameter(s) %s is unsupported.' %
            str(overridden_immutables))

    # Set notification settings for action.
    # XXX: There are cases when we don't want notifications to be sent for a particular
    # execution. So we should look at liveaction.parameters['notify']
    # and not set liveaction.notify.
    if not _is_notify_empty(action_db.notify):
        liveaction.notify = action_db.notify

    # Write to database and send to message queue.
    liveaction.status = action_constants.LIVEACTION_STATUS_REQUESTED
    liveaction.start_timestamp = date_utils.get_datetime_utc_now()

    # Set the "action_is_workflow" attribute
    liveaction.action_is_workflow = action_db.is_workflow()

    # Publish creation after both liveaction and actionexecution are created.
    liveaction = LiveAction.add_or_update(liveaction, publish=False)

    # Get trace_db if it exists. This could throw. If it throws, we have to cleanup
    # liveaction object so we don't see things in requested mode.
    trace_db = None
    try:
        _, trace_db = trace_service.get_trace_db_by_live_action(liveaction)
    except db_exc.StackStormDBObjectNotFoundError as e:
        _cleanup_liveaction(liveaction)
        raise trace_exc.TraceNotFoundException(str(e))

    execution = executions.create_execution_object(liveaction, publish=False)

    if trace_db:
        trace_service.add_or_update_given_trace_db(
            trace_db=trace_db,
            action_executions=[
                trace_service.get_trace_component_for_action_execution(
                    execution, liveaction)
            ])

    return liveaction, execution
Exemple #22
0
def create_request(liveaction, action_db=None, runnertype_db=None):
    """
    Create an action execution.

    :param action_db: Action model to operate one. If not provided, one is retrieved from the
                      database using values from "liveaction".
    :type action_db: :class:`ActionDB`

    :param runnertype_db: Runner model to operate one. If not provided, one is retrieved from the
                          database using values from "liveaction".
    :type runnertype_db: :class:`RunnerTypeDB`

    :return: (liveaction, execution)
    :rtype: tuple
    """
    # We import this here to avoid conflicts w/ runners that might import this
    # file since the runners don't have the config context by default.
    from st2common.metrics.base import get_driver

    # Use the user context from the parent action execution. Subtasks in a workflow
    # action can be invoked by a system user and so we want to use the user context
    # from the original workflow action.
    parent_context = executions.get_parent_context(liveaction) or {}
    parent_user = parent_context.get("user", None)

    if parent_user:
        liveaction.context["user"] = parent_user

    # Validate action
    if not action_db:
        action_db = action_utils.get_action_by_ref(liveaction.action)

    if not action_db:
        raise ValueError('Action "%s" cannot be found.' % liveaction.action)
    if not action_db.enabled:
        raise ValueError('Unable to execute. Action "%s" is disabled.' %
                         liveaction.action)

    if not runnertype_db:
        runnertype_db = action_utils.get_runnertype_by_name(
            action_db.runner_type["name"])

    if not hasattr(liveaction, "parameters"):
        liveaction.parameters = dict()

    # For consistency add pack to the context here in addition to RunnerContainer.dispatch() method
    liveaction.context["pack"] = action_db.pack

    # Validate action parameters.
    schema = util_schema.get_schema_for_action_parameters(
        action_db, runnertype_db)
    validator = util_schema.get_validator()
    util_schema.validate(
        liveaction.parameters,
        schema,
        validator,
        use_default=True,
        allow_default_none=True,
    )

    # validate that no immutable params are being overriden. Although possible to
    # ignore the override it is safer to inform the user to avoid surprises.
    immutables = _get_immutable_params(action_db.parameters)
    immutables.extend(_get_immutable_params(runnertype_db.runner_parameters))
    overridden_immutables = [
        p for p in six.iterkeys(liveaction.parameters) if p in immutables
    ]
    if len(overridden_immutables) > 0:
        raise ValueError(
            "Override of immutable parameter(s) %s is unsupported." %
            str(overridden_immutables))

    # Set notification settings for action.
    # XXX: There are cases when we don't want notifications to be sent for a particular
    # execution. So we should look at liveaction.parameters['notify']
    # and not set liveaction.notify.
    if not _is_notify_skipped(liveaction) and not _is_notify_empty(
            action_db.notify):
        liveaction.notify = action_db.notify

    # Write to database and send to message queue.
    liveaction.status = action_constants.LIVEACTION_STATUS_REQUESTED
    liveaction.start_timestamp = date_utils.get_datetime_utc_now()

    # Set the "action_is_workflow" attribute
    liveaction.action_is_workflow = action_db.is_workflow()

    # Publish creation after both liveaction and actionexecution are created.
    liveaction = LiveAction.add_or_update(liveaction, publish=False)
    # Get trace_db if it exists. This could throw. If it throws, we have to cleanup
    # liveaction object so we don't see things in requested mode.
    trace_db = None
    try:
        _, trace_db = trace_service.get_trace_db_by_live_action(liveaction)
    except db_exc.StackStormDBObjectNotFoundError as e:
        _cleanup_liveaction(liveaction)
        raise trace_exc.TraceNotFoundException(six.text_type(e))

    execution = executions.create_execution_object(
        liveaction=liveaction,
        action_db=action_db,
        runnertype_db=runnertype_db,
        publish=False,
    )

    if trace_db:
        trace_service.add_or_update_given_trace_db(
            trace_db=trace_db,
            action_executions=[
                trace_service.get_trace_component_for_action_execution(
                    execution, liveaction)
            ],
        )

    get_driver().inc_counter("action.executions.%s" % (liveaction.status))

    return liveaction, execution
Exemple #23
0
def request_task_execution(wf_ex_db, task_id, task_spec, task_ctx, st2_ctx):
    wf_ac_ex_id = wf_ex_db.action_execution
    LOG.info('[%s] Processing task execution request for "%s".', wf_ac_ex_id,
             task_id)

    # Create a record for task execution.
    task_ex_db = wf_db_models.TaskExecutionDB(
        workflow_execution=str(wf_ex_db.id),
        task_name=task_spec.name or task_id,
        task_id=task_id,
        task_spec=task_spec.serialize(),
        context=task_ctx,
        status=states.REQUESTED)

    # Insert new record into the database.
    task_ex_db = wf_db_access.TaskExecution.insert(task_ex_db, publish=False)
    task_ex_id = str(task_ex_db.id)
    LOG.info('[%s] Task execution "%s" created for task "%s".', wf_ac_ex_id,
             task_ex_id, task_id)

    try:
        # Return here if no action is specified in task spec.
        if task_spec.action is None:
            # Set the task execution to running.
            task_ex_db.status = states.RUNNING
            task_ex_db = wf_db_access.TaskExecution.update(task_ex_db,
                                                           publish=False)

            # Fast forward task execution to completion.
            update_task_execution(str(task_ex_db.id), states.SUCCEEDED)
            update_task_flow(str(task_ex_db.id), publish=False)

            # Refresh and return the task execution
            return wf_db_access.TaskExecution.get_by_id(str(task_ex_db.id))

        # Identify the action to execute.
        action_db = action_utils.get_action_by_ref(ref=task_spec.action)

        if not action_db:
            error = 'Unable to find action "%s".' % task_spec.action
            raise ac_exc.InvalidActionReferencedException(error)

        # Identify the runner for the action.
        runner_type_db = action_utils.get_runnertype_by_name(
            action_db.runner_type['name'])

        # Set context for the action execution.
        ac_ex_ctx = {
            'parent': st2_ctx,
            'orquesta': {
                'workflow_execution_id': str(wf_ex_db.id),
                'task_execution_id': str(task_ex_db.id),
                'task_name': task_spec.name or task_id,
                'task_id': task_id
            }
        }

        # Render action execution parameters and setup action execution object.
        ac_ex_params = param_utils.render_live_params(
            runner_type_db.runner_parameters or {}, action_db.parameters or {},
            getattr(task_spec, 'input', None) or {}, ac_ex_ctx)

        lv_ac_db = lv_db_models.LiveActionDB(action=task_spec.action,
                                             workflow_execution=str(
                                                 wf_ex_db.id),
                                             task_execution=str(task_ex_db.id),
                                             context=ac_ex_ctx,
                                             parameters=ac_ex_params)

        # Set the task execution to running first otherwise a race can occur
        # where the action execution finishes first and the completion handler
        # conflicts with this status update.
        task_ex_db.status = states.RUNNING
        task_ex_db = wf_db_access.TaskExecution.update(task_ex_db,
                                                       publish=False)

        # Request action execution.
        lv_ac_db, ac_ex_db = ac_svc.request(lv_ac_db)

        msg = '[%s] Action execution "%s" requested for task "%s".'
        LOG.info(msg, wf_ac_ex_id, str(ac_ex_db.id), task_id)
    except Exception as e:
        LOG.exception('[%s] Failed task execution for task "%s".', wf_ac_ex_id,
                      task_id)
        result = {
            'errors': [{
                'message': str(e),
                'task_id': task_ex_db.task_id
            }]
        }
        update_task_execution(str(task_ex_db.id), states.FAILED, result)
        raise e

    return task_ex_db
Exemple #24
0
    def enforce(self):
        # TODO: Refactor this to avoid additional lookup in cast_params
        # TODO: rename self.rule.action -> self.rule.action_exec_spec
        action_ref = self.rule.action['ref']
        action_db = action_db_util.get_action_by_ref(action_ref)
        if not action_db:
            raise ValueError('Action "%s" doesn\'t exist' % (action_ref))

        data = self.data_transformer(self.rule.action.parameters)
        LOG.info('Invoking action %s for trigger_instance %s with data %s.',
                 self.rule.action.ref, self.trigger_instance.id,
                 json.dumps(data))

        # update trace before invoking the action.
        trace_context = self._update_trace()
        LOG.debug('Updated trace %s with rule %s.', trace_context,
                  self.rule.id)

        context = {
            'trigger_instance':
            reference.get_ref_from_model(self.trigger_instance),
            'rule': reference.get_ref_from_model(self.rule),
            'user': get_system_username(),
            TRACE_CONTEXT: trace_context
        }

        extra = {
            'trigger_instance_db': self.trigger_instance,
            'rule_db': self.rule
        }
        rule_spec = {
            'ref': self.rule.ref,
            'id': str(self.rule.id),
            'uid': self.rule.uid
        }
        enforcement_db = RuleEnforcementDB(trigger_instance_id=str(
            self.trigger_instance.id),
                                           rule=rule_spec)
        try:
            execution_db = RuleEnforcer._invoke_action(self.rule.action, data,
                                                       context)
            # pylint: disable=no-member
            enforcement_db.execution_id = str(execution_db.id)
            # pylint: enable=no-member
        except:
            LOG.exception('Failed kicking off execution for rule %s.',
                          self.rule,
                          extra=extra)
            return None
        finally:
            self._update_enforcement(enforcement_db)

        extra['execution_db'] = execution_db
        # pylint: disable=no-member
        if execution_db.status not in EXEC_KICKED_OFF_STATES:
            # pylint: enable=no-member
            LOG.audit(
                'Rule enforcement failed. Execution of Action %s failed. '
                'TriggerInstance: %s and Rule: %s',
                self.rule.action.name,
                self.trigger_instance,
                self.rule,
                extra=extra)
            return execution_db

        LOG.audit(
            'Rule enforced. Execution %s, TriggerInstance %s and Rule %s.',
            execution_db,
            self.trigger_instance,
            self.rule,
            extra=extra)

        return execution_db
Exemple #25
0
    def _schedule_execution(self,
                            liveaction,
                            requester_user,
                            user=None,
                            context_string=None,
                            show_secrets=False,
                            pack=None):
        # Initialize execution context if it does not exist.
        if not hasattr(liveaction, 'context'):
            liveaction.context = dict()

        liveaction.context['user'] = user
        liveaction.context['pack'] = pack
        LOG.debug('User is: %s' % liveaction.context['user'])

        # Retrieve other st2 context from request header.
        if context_string:
            context = try_loads(context_string)
            if not isinstance(context, dict):
                raise ValueError(
                    'Unable to convert st2-context from the headers into JSON.'
                )
            liveaction.context.update(context)

        # Include RBAC context (if RBAC is available and enabled)
        if cfg.CONF.rbac.enable:
            user_db = UserDB(name=user)
            role_dbs = rbac_service.get_roles_for_user(user_db=user_db,
                                                       include_remote=True)
            roles = [role_db.name for role_db in role_dbs]
            liveaction.context['rbac'] = {'user': user, 'roles': roles}

        # Schedule the action execution.
        liveaction_db = LiveActionAPI.to_model(liveaction)
        action_db = action_utils.get_action_by_ref(liveaction_db.action)
        runnertype_db = action_utils.get_runnertype_by_name(
            action_db.runner_type['name'])

        try:
            liveaction_db.parameters = param_utils.render_live_params(
                runnertype_db.runner_parameters, action_db.parameters,
                liveaction_db.parameters, liveaction_db.context)
        except param_exc.ParamException:

            # We still need to create a request, so liveaction_db is assigned an ID
            liveaction_db, actionexecution_db = action_service.create_request(
                liveaction_db)

            # By this point the execution is already in the DB therefore need to mark it failed.
            _, e, tb = sys.exc_info()
            action_service.update_status(
                liveaction=liveaction_db,
                new_status=action_constants.LIVEACTION_STATUS_FAILED,
                result={
                    'error': str(e),
                    'traceback': ''.join(traceback.format_tb(tb, 20))
                })
            # Might be a good idea to return the actual ActionExecution rather than bubble up
            # the exception.
            raise validation_exc.ValueValidationException(str(e))

        # The request should be created after the above call to render_live_params
        # so any templates in live parameters have a chance to render.
        liveaction_db, actionexecution_db = action_service.create_request(
            liveaction_db)
        liveaction_db = LiveAction.add_or_update(liveaction_db, publish=False)

        _, actionexecution_db = action_service.publish_request(
            liveaction_db, actionexecution_db)
        mask_secrets = self._get_mask_secrets(requester_user,
                                              show_secrets=show_secrets)
        execution_api = ActionExecutionAPI.from_model(
            actionexecution_db, mask_secrets=mask_secrets)

        return Response(json=execution_api, status=http_client.CREATED)
Exemple #26
0
def create_execution_object(liveaction,
                            action_db=None,
                            runnertype_db=None,
                            publish=True):
    if not action_db:
        action_db = action_utils.get_action_by_ref(liveaction.action)

    if not runnertype_db:
        runnertype_db = RunnerType.get_by_name(action_db.runner_type["name"])

    attrs = {
        "action": vars(ActionAPI.from_model(action_db)),
        "parameters": liveaction["parameters"],
        "runner": vars(RunnerTypeAPI.from_model(runnertype_db)),
    }
    attrs.update(_decompose_liveaction(liveaction))

    if "rule" in liveaction.context:
        rule = reference.get_model_from_ref(Rule,
                                            liveaction.context.get("rule", {}))
        attrs["rule"] = vars(RuleAPI.from_model(rule))

    if "trigger_instance" in liveaction.context:
        trigger_instance_id = liveaction.context.get("trigger_instance", {})
        trigger_instance_id = trigger_instance_id.get("id", None)
        trigger_instance = TriggerInstance.get_by_id(trigger_instance_id)
        trigger = reference.get_model_by_resource_ref(
            db_api=Trigger, ref=trigger_instance.trigger)
        trigger_type = reference.get_model_by_resource_ref(db_api=TriggerType,
                                                           ref=trigger.type)
        trigger_instance = reference.get_model_from_ref(
            TriggerInstance, liveaction.context.get("trigger_instance", {}))
        attrs["trigger_instance"] = vars(
            TriggerInstanceAPI.from_model(trigger_instance))
        attrs["trigger"] = vars(TriggerAPI.from_model(trigger))
        attrs["trigger_type"] = vars(TriggerTypeAPI.from_model(trigger_type))

    parent = _get_parent_execution(liveaction)
    if parent:
        attrs["parent"] = str(parent.id)

    attrs["log"] = [_create_execution_log_entry(liveaction["status"])]

    # TODO: This object initialization takes 20-30or so ms
    execution = ActionExecutionDB(**attrs)
    # TODO: Do 100% research this is fully safe and unique in distributed setups
    execution.id = ObjectId()
    execution.web_url = _get_web_url_for_execution(str(execution.id))

    # NOTE: User input data is already validate as part of the API request,
    # other data is set by us. Skipping validation here makes operation 10%-30% faster
    execution = ActionExecution.add_or_update(execution,
                                              publish=publish,
                                              validate=False)

    if parent and str(execution.id) not in parent.children:
        values = {}
        values["push__children"] = str(execution.id)
        ActionExecution.update(parent, **values)

    return execution