From 0d3153f774c71b1b459fd4b9c2ab0d6d4ef5c848 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 11:08:14 +0200 Subject: [PATCH 01/40] Fix code so it works under Python 3. --- st2reactor/st2reactor/rules/enforcer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2reactor/st2reactor/rules/enforcer.py b/st2reactor/st2reactor/rules/enforcer.py index 13b814aabd..b629527363 100644 --- a/st2reactor/st2reactor/rules/enforcer.py +++ b/st2reactor/st2reactor/rules/enforcer.py @@ -97,7 +97,7 @@ def enforce(self): extra['execution_db'] = execution_db except Exception as e: # Record the failure reason in the RuleEnforcement. - enforcement_db.failure_reason = e.message + enforcement_db.failure_reason = str(e) LOG.exception('Failed kicking off execution for rule %s.', self.rule, extra=extra) finally: self._update_enforcement(enforcement_db) From 7ab7e1b84d8e68b0dc12c1d4b8db04118985d3f3 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 11:24:41 +0200 Subject: [PATCH 02/40] Also create RuleEnforcementDB objects for trigger instances which failed to match rule inside the rules filtering phase due to an exception. This way user has more visibility into matches which failed because of an exception (failed to render Jinja expression, etc). Without this change, only way to find those rules is by checking rules engine service log. --- st2reactor/st2reactor/rules/filter.py | 55 ++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py index 5ad73a2c2f..1f79ea8448 100644 --- a/st2reactor/st2reactor/rules/filter.py +++ b/st2reactor/st2reactor/rules/filter.py @@ -14,17 +14,26 @@ # limitations under the License. from __future__ import absolute_import -import six + import json import re +import six + from st2common import log as logging -import st2common.operators as criteria_operators -from st2common.constants.rules import RULE_TYPE_BACKSTOP, MATCH_CRITERIA +from st2common import operators as criteria_operators +from st2common.constants.rules import RULE_TYPE_BACKSTOP +from st2common.constants.rules import MATCH_CRITERIA +from st2common.models.db.rule_enforcement import RuleEnforcementDB +from st2common.persistence.rule_enforcement import RuleEnforcement from st2common.util.payload import PayloadLookup from st2common.util.templating import render_template_with_system_context +__all__ = [ + 'RuleFilter' +] + LOG = logging.getLogger('st2reactor.ruleenforcement.filter') @@ -119,8 +128,11 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): criteria_context=payload_lookup.context ) except Exception: - LOG.exception('Failed to render pattern value "%s" for key "%s"' % - (criteria_pattern, criterion_k), extra=self._base_logger_context) + msg = ('Failed to render pattern value "%s" for key "%s"' % (criteria_pattern, + criterion_k)) + LOG.exception(msg, extra=self._base_logger_context) + self._create_rule_enforcement(failure_reason=msg) + return (False, None, None) try: @@ -131,8 +143,10 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): else: payload_value = None except: - LOG.exception('Failed transforming criteria key %s', criterion_k, - extra=self._base_logger_context) + msg = ('Failed transforming criteria key %s' % criterion_k) + LOG.exception(msg, extra=self._base_logger_context) + self._create_rule_enforcement(failure_reason=msg) + return (False, None, None) op_func = criteria_operators.get_operator(criteria_operator) @@ -145,8 +159,10 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): else: result = op_func(value=payload_value, criteria_pattern=criteria_pattern) except: - LOG.exception('There might be a problem with the criteria in rule %s.', self.rule, - extra=self._base_logger_context) + msg = ('There might be a problem with the criteria in rule %s.' % self.rule) + LOG.exception(msg, extra=self._base_logger_context) + self._create_rule_enforcement(failure_reason=msg) + return (False, None, None) return result, payload_value, criteria_pattern @@ -207,6 +223,27 @@ def _render_criteria_pattern(self, criteria_pattern, criteria_context): return criteria_rendered + def _create_rule_enforcement(self, failure_reason): + """ + Note: We also create RuleEnforcementDB for rules which failed to match due to an exception. + + Without that, only way for users to find out about those failes matches is by inspecting + the logs. + """ + failure_reason = ('Failed to match rule "%s" against trigger instance "%s": %s' % + (self.rule.ref, str(self.trigger_instance.id), failure_reason)) + enforcement_db = RuleEnforcementDB(trigger_instance_id=str(self.trigger_instance.id), + rule=self.rule, + failure_reason=failure_reason) + + try: + RuleEnforcement.add_or_update(enforcement_db) + except: + extra = {'enforcement_db': enforcement_db} + LOG.exception('Failed writing enforcement model to db.', extra=extra) + + return enforcement_db + class SecondPassRuleFilter(RuleFilter): """ From 734f45a9ea0cb2c3adb05df13e88b639da4e3fe0 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 12:40:56 +0200 Subject: [PATCH 03/40] Correctly use rule_spec dictionary. --- st2reactor/st2reactor/rules/filter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py index 1f79ea8448..10943faec6 100644 --- a/st2reactor/st2reactor/rules/filter.py +++ b/st2reactor/st2reactor/rules/filter.py @@ -232,8 +232,9 @@ def _create_rule_enforcement(self, failure_reason): """ failure_reason = ('Failed to match rule "%s" against trigger instance "%s": %s' % (self.rule.ref, str(self.trigger_instance.id), failure_reason)) + 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=self.rule, + rule=rule_spec, failure_reason=failure_reason) try: From 71c1ae684307cb62382dc2292618f6251a35e7ac Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 12:46:13 +0200 Subject: [PATCH 04/40] Add test cases for it. Also fix existing test cases so they don't rely on each other and can be run indepdently of each other. --- st2reactor/tests/unit/test_rule_matcher.py | 312 +++++++++++++++------ 1 file changed, 219 insertions(+), 93 deletions(-) diff --git a/st2reactor/tests/unit/test_rule_matcher.py b/st2reactor/tests/unit/test_rule_matcher.py index 70368de309..5a7ea248ac 100644 --- a/st2reactor/tests/unit/test_rule_matcher.py +++ b/st2reactor/tests/unit/test_rule_matcher.py @@ -14,7 +14,9 @@ # limitations under the License. from __future__ import absolute_import + import six +import mock from st2common.models.api.rule import RuleAPI from st2common.models.db.trigger import (TriggerDB, TriggerTypeDB) @@ -24,22 +26,131 @@ from st2common.util import date as date_utils import st2reactor.container.utils as container_utils from st2reactor.rules.matcher import RulesMatcher +from st2common.persistence.rule_enforcement import RuleEnforcement + from st2tests.base import DbTestCase +from st2tests.base import CleanDbTestCase from st2tests.fixturesloader import FixturesLoader +__all__ = [ + 'RuleMatcherTestCase', + 'BackstopRuleMatcherTestCase' +] + +# Mock rules +RULE_1 = { + 'enabled': True, + 'name': 'st2.test.rule1', + 'pack': 'yoyohoneysingh', + 'trigger': { + 'type': 'dummy_pack_1.st2.test.trigger1' + }, + 'criteria': { + 'k1': { # Missing prefix 'trigger'. This rule won't match. + 'pattern': 't1_p_v', + 'type': 'equals' + } + }, + 'action': { + 'ref': 'sixpack.st2.test.action', + 'parameters': { + 'ip2': '{{rule.k1}}', + 'ip1': '{{trigger.t1_p}}' + } + }, + 'id': '23', + 'description': '' +} + +RULE_2 = { # Rule should match. + 'enabled': True, + 'name': 'st2.test.rule2', + 'pack': 'yoyohoneysingh', + 'trigger': { + 'type': 'dummy_pack_1.st2.test.trigger1' + }, + 'criteria': { + 'trigger.k1': { + 'pattern': 't1_p_v', + 'type': 'equals' + } + }, + 'action': { + 'ref': 'sixpack.st2.test.action', + 'parameters': { + 'ip2': '{{rule.k1}}', + 'ip1': '{{trigger.t1_p}}' + } + }, + 'id': '23', + 'description': '' +} + +RULE_3 = { + 'enabled': False, # Disabled rule shouldn't match. + 'name': 'st2.test.rule3', + 'pack': 'yoyohoneysingh', + 'trigger': { + 'type': 'dummy_pack_1.st2.test.trigger1' + }, + 'criteria': { + 'trigger.k1': { + 'pattern': 't1_p_v', + 'type': 'equals' + } + }, + 'action': { + 'ref': 'sixpack.st2.test.action', + 'parameters': { + 'ip2': '{{rule.k1}}', + 'ip1': '{{trigger.t1_p}}' + } + }, + 'id': '23', + 'description': '' +} + +RULE_4 = { # Rule should match. + 'enabled': True, + 'name': 'st2.test.rule4', + 'pack': 'yoyohoneysingh', + 'trigger': { + 'type': 'dummy_pack_1.st2.test.trigger4' + }, + 'criteria': { + 'trigger.k1': { + 'pattern': 't2_p_v', + 'type': 'equals' + } + }, + 'action': { + 'ref': 'sixpack.st2.test.action', + 'parameters': { + 'ip2': '{{rule.k1}}', + 'ip1': '{{trigger.t1_p}}' + } + }, + 'id': '23', + 'description': '' +} + -class RuleMatcherTest(DbTestCase): +class RuleMatcherTestCase(CleanDbTestCase): rules = [] def test_get_matching_rules(self): self._setup_sample_trigger('st2.test.trigger1') + rule_db_1 = self._setup_sample_rule(RULE_1) + rule_db_2 = self._setup_sample_rule(RULE_2) + rule_db_3 = self._setup_sample_rule(RULE_3) + rules = [rule_db_1, rule_db_2, rule_db_3] trigger_instance = container_utils.create_trigger_instance( 'dummy_pack_1.st2.test.trigger1', {'k1': 't1_p_v', 'k2': 'v2'}, date_utils.get_datetime_utc_now() ) + trigger = get_trigger_db_by_ref(trigger_instance.trigger) - rules = self._get_sample_rules() rules_matcher = RulesMatcher(trigger_instance, trigger, rules) matching_rules = rules_matcher.get_matching_rules() self.assertTrue(matching_rules is not None) @@ -47,20 +158,120 @@ def test_get_matching_rules(self): def test_trigger_instance_payload_with_special_values(self): # Test a rule where TriggerInstance payload contains a dot (".") and $ + self._setup_sample_trigger('st2.test.trigger1') self._setup_sample_trigger('st2.test.trigger2') + rule_db_1 = self._setup_sample_rule(RULE_1) + rule_db_2 = self._setup_sample_rule(RULE_2) + rule_db_3 = self._setup_sample_rule(RULE_3) + rules = [rule_db_1, rule_db_2, rule_db_3] trigger_instance = container_utils.create_trigger_instance( 'dummy_pack_1.st2.test.trigger2', {'k1': 't1_p_v', 'k2.k2': 'v2', 'k3.more.nested.deep': 'some.value', 'k4.even.more.nested$': 'foo', 'yep$aaa': 'b'}, date_utils.get_datetime_utc_now() ) + trigger = get_trigger_db_by_ref(trigger_instance.trigger) - rules = self._get_sample_rules() rules_matcher = RulesMatcher(trigger_instance, trigger, rules) matching_rules = rules_matcher.get_matching_rules() self.assertTrue(matching_rules is not None) self.assertEqual(len(matching_rules), 1) + @mock.patch('st2reactor.rules.matcher.RuleFilter._render_criteria_pattern', + mock.Mock(side_effect=Exception('exception in _render_criteria_pattern'))) + def test_rule_enforcement_is_created_on_exception_1(self): + # 1. Exception in _render_criteria_pattern + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(rule_enforcement_dbs, []) + + self._setup_sample_trigger('st2.test.trigger4') + rule_4_db = self._setup_sample_rule(RULE_4) + rules = [rule_4_db] + trigger_instance = container_utils.create_trigger_instance( + 'dummy_pack_1.st2.test.trigger4', + {'k1': 't2_p_v', 'k2': 'v2'}, + date_utils.get_datetime_utc_now() + ) + trigger = get_trigger_db_by_ref(trigger_instance.trigger) + + rules_matcher = RulesMatcher(trigger_instance, trigger, rules) + matching_rules = rules_matcher.get_matching_rules() + self.assertEqual(matching_rules, []) + self.assertEqual(len(matching_rules), 0) + + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(len(rule_enforcement_dbs), 1) + + expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' + 'instance "%s": Failed to render pattern value "t2_p_v" for key ' + '"trigger.k1"' % (str(trigger_instance.id))) + self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) + self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) + self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + + @mock.patch('st2reactor.rules.filter.PayloadLookup.get_value', + mock.Mock(side_effect=Exception('exception in get_value'))) + def test_rule_enforcement_is_created_on_exception_2(self): + # 1. Exception in payload_lookup.get_value + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(rule_enforcement_dbs, []) + + self._setup_sample_trigger('st2.test.trigger4') + rule_4_db = self._setup_sample_rule(RULE_4) + rules = [rule_4_db] + trigger_instance = container_utils.create_trigger_instance( + 'dummy_pack_1.st2.test.trigger4', + {'k1': 't2_p_v', 'k2': 'v2'}, + date_utils.get_datetime_utc_now() + ) + trigger = get_trigger_db_by_ref(trigger_instance.trigger) + + rules_matcher = RulesMatcher(trigger_instance, trigger, rules) + matching_rules = rules_matcher.get_matching_rules() + self.assertEqual(matching_rules, []) + self.assertEqual(len(matching_rules), 0) + + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(len(rule_enforcement_dbs), 1) + + expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' + 'instance "%s": Failed transforming criteria key trigger.k1' % (str(trigger_instance.id))) + self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) + self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) + self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + + @mock.patch('st2common.operators.get_operator', + mock.Mock(return_value=mock.Mock(side_effect=Exception('exception in equals')))) + def test_rule_enforcement_is_created_on_exception_3(self): + # 1. Exception in payload_lookup.get_value + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(rule_enforcement_dbs, []) + + self._setup_sample_trigger('st2.test.trigger4') + rule_4_db = self._setup_sample_rule(RULE_4) + rules = [rule_4_db] + trigger_instance = container_utils.create_trigger_instance( + 'dummy_pack_1.st2.test.trigger4', + {'k1': 't2_p_v', 'k2': 'v2'}, + date_utils.get_datetime_utc_now() + ) + trigger = get_trigger_db_by_ref(trigger_instance.trigger) + + rules_matcher = RulesMatcher(trigger_instance, trigger, rules) + matching_rules = rules_matcher.get_matching_rules() + self.assertEqual(matching_rules, []) + self.assertEqual(len(matching_rules), 0) + + rule_enforcement_dbs = list(RuleEnforcement.get_all()) + self.assertEqual(len(rule_enforcement_dbs), 1) + + expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' + 'instance "%s": There might be a problem with the criteria in rule ' + 'yoyohoneysingh.st2.test.rule4.' % (str(trigger_instance.id))) + self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) + self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) + self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + def _setup_sample_trigger(self, name): trigtype = TriggerTypeDB(name=name, pack='dummy_pack_1', payload_schema={}, parameters_schema={}) @@ -70,96 +281,11 @@ def _setup_sample_trigger(self, name): parameters={}) Trigger.add_or_update(created) - def _get_sample_rules(self): - if self.rules: - # Make sure rules are created only once - return self.rules - - RULE_1 = { - 'enabled': True, - 'name': 'st2.test.rule1', - 'pack': 'yoyohoneysingh', - 'trigger': { - 'type': 'dummy_pack_1.st2.test.trigger1' - }, - 'criteria': { - 'k1': { # Missing prefix 'trigger'. This rule won't match. - 'pattern': 't1_p_v', - 'type': 'equals' - } - }, - 'action': { - 'ref': 'sixpack.st2.test.action', - 'parameters': { - 'ip2': '{{rule.k1}}', - 'ip1': '{{trigger.t1_p}}' - } - }, - 'id': '23', - 'description': '' - } - rule_api = RuleAPI(**RULE_1) + def _setup_sample_rule(self, rule): + rule_api = RuleAPI(**rule) rule_db = RuleAPI.to_model(rule_api) rule_db = Rule.add_or_update(rule_db) - self.rules.append(rule_db) - - RULE_2 = { # Rule should match. - 'enabled': True, - 'name': 'st2.test.rule2', - 'pack': 'yoyohoneysingh', - 'trigger': { - 'type': 'dummy_pack_1.st2.test.trigger1' - }, - 'criteria': { - 'trigger.k1': { - 'pattern': 't1_p_v', - 'type': 'equals' - } - }, - 'action': { - 'ref': 'sixpack.st2.test.action', - 'parameters': { - 'ip2': '{{rule.k1}}', - 'ip1': '{{trigger.t1_p}}' - } - }, - 'id': '23', - 'description': '' - } - rule_api = RuleAPI(**RULE_2) - rule_db = RuleAPI.to_model(rule_api) - rule_db = Rule.add_or_update(rule_db) - self.rules.append(rule_db) - - RULE_3 = { - 'enabled': False, # Disabled rule shouldn't match. - 'name': 'st2.test.rule3', - 'pack': 'yoyohoneysingh', - 'trigger': { - 'type': 'dummy_pack_1.st2.test.trigger1' - }, - 'criteria': { - 'trigger.k1': { - 'pattern': 't1_p_v', - 'type': 'equals' - } - }, - 'action': { - 'ref': 'sixpack.st2.test.action', - 'parameters': { - 'ip2': '{{rule.k1}}', - 'ip1': '{{trigger.t1_p}}' - } - }, - 'id': '23', - 'description': '' - } - rule_api = RuleAPI(**RULE_3) - rule_db = RuleAPI.to_model(rule_api) - rule_db = Rule.add_or_update(rule_db) - self.rules.append(rule_db) - - return self.rules + return rule_db PACK = 'backstop' @@ -172,12 +298,12 @@ def _get_sample_rules(self): } -class BackstopRuleMatcherTest(DbTestCase): +class BackstopRuleMatcherTestCase(DbTestCase): models = None @classmethod def setUpClass(cls): - super(BackstopRuleMatcherTest, cls).setUpClass() + super(BackstopRuleMatcherTestCase, cls).setUpClass() fixturesloader = FixturesLoader() # Create TriggerTypes before creation of Rule to avoid failure. Rule requires the # Trigger and therefore TriggerType to be created prior to rule creation. From 09195cddd60e7269c58fe52d419f04762051efc7 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 12:51:09 +0200 Subject: [PATCH 05/40] Include original exception message in the failure_reason and update affected tests. --- st2reactor/st2reactor/rules/filter.py | 20 ++++++++++---------- st2reactor/tests/unit/test_rule_matcher.py | 9 ++++++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py index 10943faec6..83ce913214 100644 --- a/st2reactor/st2reactor/rules/filter.py +++ b/st2reactor/st2reactor/rules/filter.py @@ -127,11 +127,11 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): criteria_pattern=criteria_pattern, criteria_context=payload_lookup.context ) - except Exception: + except Exception as e: msg = ('Failed to render pattern value "%s" for key "%s"' % (criteria_pattern, criterion_k)) LOG.exception(msg, extra=self._base_logger_context) - self._create_rule_enforcement(failure_reason=msg) + self._create_rule_enforcement(failure_reason=msg, exc=e) return (False, None, None) @@ -142,10 +142,10 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): payload_value = matches[0] if len(matches) > 0 else matches else: payload_value = None - except: + except Exception as e: msg = ('Failed transforming criteria key %s' % criterion_k) LOG.exception(msg, extra=self._base_logger_context) - self._create_rule_enforcement(failure_reason=msg) + self._create_rule_enforcement(failure_reason=msg, exc=e) return (False, None, None) @@ -158,10 +158,10 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): check_function=self._bool_criterion) else: result = op_func(value=payload_value, criteria_pattern=criteria_pattern) - except: - msg = ('There might be a problem with the criteria in rule %s.' % self.rule) + except Exception as e: + msg = ('There might be a problem with the criteria in rule %s' % (self.rule.ref)) LOG.exception(msg, extra=self._base_logger_context) - self._create_rule_enforcement(failure_reason=msg) + self._create_rule_enforcement(failure_reason=msg, exc=e) return (False, None, None) @@ -223,15 +223,15 @@ def _render_criteria_pattern(self, criteria_pattern, criteria_context): return criteria_rendered - def _create_rule_enforcement(self, failure_reason): + def _create_rule_enforcement(self, failure_reason, exc): """ Note: We also create RuleEnforcementDB for rules which failed to match due to an exception. Without that, only way for users to find out about those failes matches is by inspecting the logs. """ - failure_reason = ('Failed to match rule "%s" against trigger instance "%s": %s' % - (self.rule.ref, str(self.trigger_instance.id), failure_reason)) + failure_reason = ('Failed to match rule "%s" against trigger instance "%s": %s: %s' % + (self.rule.ref, str(self.trigger_instance.id), failure_reason, str(exc))) 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, diff --git a/st2reactor/tests/unit/test_rule_matcher.py b/st2reactor/tests/unit/test_rule_matcher.py index 5a7ea248ac..a42b8ff7bb 100644 --- a/st2reactor/tests/unit/test_rule_matcher.py +++ b/st2reactor/tests/unit/test_rule_matcher.py @@ -204,7 +204,8 @@ def test_rule_enforcement_is_created_on_exception_1(self): expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' 'instance "%s": Failed to render pattern value "t2_p_v" for key ' - '"trigger.k1"' % (str(trigger_instance.id))) + '"trigger.k1": exception in _render_criteria_pattern' % + (str(trigger_instance.id))) self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) @@ -235,7 +236,8 @@ def test_rule_enforcement_is_created_on_exception_2(self): self.assertEqual(len(rule_enforcement_dbs), 1) expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' - 'instance "%s": Failed transforming criteria key trigger.k1' % (str(trigger_instance.id))) + 'instance "%s": Failed transforming criteria key trigger.k1: ' + 'exception in get_value' % (str(trigger_instance.id))) self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) @@ -267,7 +269,8 @@ def test_rule_enforcement_is_created_on_exception_3(self): expected_failure = ('Failed to match rule "yoyohoneysingh.st2.test.rule4" against trigger ' 'instance "%s": There might be a problem with the criteria in rule ' - 'yoyohoneysingh.st2.test.rule4.' % (str(trigger_instance.id))) + 'yoyohoneysingh.st2.test.rule4: exception in equals' % + (str(trigger_instance.id))) self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) From 13e20754ccf5d5b194bceabecfdd187353ce6648 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 13:13:22 +0200 Subject: [PATCH 06/40] Add changelog entry. --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da5fff73d4..71bdf7e8e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,12 @@ Added and functions. (new feature) #4004 #2974 * When running a dev (unstable) release include git revision hash in the output when using ``st2 --version`` CLI command. (new feature) #4117 +* Update rules engine to also create rule enforcement object when trigger instances fails to match + a rule during the rule matching / filtering phase due to an exception in the rule criteria (e.g. + invalid Jinja expression, etc.). + + This change increases visibility into rules which didn't match due to an exception. Previously + this was only visible / reflected in the rules engine log file. (improvement) #4134 Changed ~~~~~~~ From e659872488bd7528531e1165711048dead4c7955 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:15:21 +0200 Subject: [PATCH 07/40] Re-generate requirements.txt, use specific mock version. --- fixed-requirements.txt | 1 + requirements.txt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/fixed-requirements.txt b/fixed-requirements.txt index 510b3bb857..72eec67c17 100644 --- a/fixed-requirements.txt +++ b/fixed-requirements.txt @@ -47,3 +47,4 @@ nose-timer>=0.7.2,<0.8 psutil==5.4.5 python-statsd==2.1.0 prometheus_client==0.1.1 +mock==2.0.0 diff --git a/requirements.txt b/requirements.txt index eb9b4593dd..72946dc83f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonpath-rw==1.4.0 jsonschema==2.6.0 kombu==4.1.0 lockfile==0.12.2 -mock +mock==2.0.0 mongoengine==0.12.0 networkx==1.11 nose @@ -26,6 +26,7 @@ oslo.utils<=3.33.0,>=3.15.0 paramiko==2.4.1 passlib==1.7.1 prettytable +prometheus_client==0.1.1 prompt-toolkit==1.0.15 psutil==5.4.5 pyinotify==0.9.6 @@ -35,6 +36,7 @@ python-editor==1.0.3 python-gnupg==0.4.2 python-json-logger python-keyczar==0.716 +python-statsd==2.1.0 pytz pyyaml<4.0,>=3.12 rednose From 318886449784d18bd8f34a6576df5643bd10d3ab Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:19:15 +0200 Subject: [PATCH 08/40] Add missing RandomWords to st2tests in requirements. --- requirements.txt | 1 + st2tests/in-requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 72946dc83f..b40e2c783e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # Don't edit this file. It's generated automatically! +RandomWords apscheduler==3.5.1 argcomplete bcrypt diff --git a/st2tests/in-requirements.txt b/st2tests/in-requirements.txt index 46023e4f09..b9b8489557 100644 --- a/st2tests/in-requirements.txt +++ b/st2tests/in-requirements.txt @@ -6,3 +6,4 @@ psutil webtest nose-timer rednose +RandomWords From 9ce1668ba93891ff97372789500b19f3c805ec35 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:28:41 +0200 Subject: [PATCH 09/40] Also run st2reactor tests under Python 3. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6301c4d144..1ee664fa9c 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ deps = virtualenv -e{toxinidir}/st2common commands = nosetests --with-timer --rednose -sv st2client/tests/unit + nosetests --with-timer --rednose -sv st2reactor/tests/unit nosetests --with-timer --rednose -sv --ignore-files=test_kvps.* st2api/tests/unit/controllers/v1/ nosetests --with-timer --rednose -sv --ignore-files=test_validator_mistral.* st2api/tests/unit/controllers/exp/ nosetests --with-timer --rednose -sv --ignore-files=test_jinja_render_crypto_filters.* --ignore-files=test_config_loader.* --ignore-files=test_crypto_utils.* --ignore-files=test_db.* --ignore-files=test_logging.* st2common/tests/unit/ From cba6f04637bab15af29e4b05c99523c50884a078 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:50:39 +0200 Subject: [PATCH 10/40] Give it more time for garbage collection to avoid false negatives under Python 3. --- st2reactor/tests/integration/test_garbage_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2reactor/tests/integration/test_garbage_collector.py b/st2reactor/tests/integration/test_garbage_collector.py index edb4660eb4..618ec238ac 100644 --- a/st2reactor/tests/integration/test_garbage_collector.py +++ b/st2reactor/tests/integration/test_garbage_collector.py @@ -180,7 +180,7 @@ def test_garbage_collection(self): process = self._start_garbage_collector() # Give it some time to perform garbage collection and kill it - eventlet.sleep(5) + eventlet.sleep(8) process.send_signal(signal.SIGKILL) self.remove_process(process=process) From 0dc2094e4c6ba7d1cac146ee6cdad39fc11e4ab8 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:52:19 +0200 Subject: [PATCH 11/40] Add missing pyrabbit to st2tests in-requirements. --- requirements.txt | 1 + st2tests/in-requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index b40e2c783e..c8c54c9f22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,7 @@ prompt-toolkit==1.0.15 psutil==5.4.5 pyinotify==0.9.6 pymongo==3.6.1 +pyrabbit python-dateutil python-editor==1.0.3 python-gnupg==0.4.2 diff --git a/st2tests/in-requirements.txt b/st2tests/in-requirements.txt index b9b8489557..ab9c28e1f0 100644 --- a/st2tests/in-requirements.txt +++ b/st2tests/in-requirements.txt @@ -7,3 +7,4 @@ webtest nose-timer rednose RandomWords +pyrabbit From 5dd422dd23157fdc2e4a1464413335cbbd6ab0f2 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:54:30 +0200 Subject: [PATCH 12/40] More Python 3 compatibility fixes for tests. --- st2reactor/tests/integration/test_rules_engine.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/st2reactor/tests/integration/test_rules_engine.py b/st2reactor/tests/integration/test_rules_engine.py index 08bcb635ad..44c08c5a7f 100644 --- a/st2reactor/tests/integration/test_rules_engine.py +++ b/st2reactor/tests/integration/test_rules_engine.py @@ -20,7 +20,8 @@ from eventlet.green import subprocess -from st2common.constants.timer import TIMER_ENABLED_LOG_LINE, TIMER_DISABLED_LOG_LINE +from st2common.constants.timer import TIMER_ENABLED_LOG_LINE +from st2common.constants.timer import TIMER_DISABLED_LOG_LINE from st2tests.base import IntegrationTestCase from st2tests.base import CleanDbTestCase @@ -65,7 +66,7 @@ def test_timer_enable_implicit(self): while lines < 100: line = process.stdout.readline() lines += 1 - if TIMER_ENABLED_LOG_LINE in line: + if TIMER_ENABLED_LOG_LINE in line.decode('utf-8'): self.assertTrue(True) break finally: @@ -82,7 +83,7 @@ def test_timer_enable_explicit(self): while lines < 100: line = process.stdout.readline() lines += 1 - if TIMER_ENABLED_LOG_LINE in line: + if TIMER_ENABLED_LOG_LINE in line.decode('utf-8'): self.assertTrue(True) break finally: @@ -99,7 +100,7 @@ def test_timer_disable_explicit(self): while lines < 100: line = process.stdout.readline() lines += 1 - if TIMER_DISABLED_LOG_LINE in line: + if TIMER_DISABLED_LOG_LINE in line.decode('utf-8'): self.assertTrue(True) break finally: From 392aca1ebf439a0e147ffb25ad80ace8091fde41 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 17:54:59 +0200 Subject: [PATCH 13/40] Also run st2reactor integration tests under Python 3. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1ee664fa9c..d8add89eb5 100644 --- a/tox.ini +++ b/tox.ini @@ -36,8 +36,9 @@ deps = virtualenv -e{toxinidir}/st2client -e{toxinidir}/st2common commands = - nosetests --with-timer --rednose -sv st2client/tests/unit - nosetests --with-timer --rednose -sv st2reactor/tests/unit + nosetests --with-timer --rednose -sv st2client/tests/unit/ + nosetests --with-timer --rednose -sv st2reactor/tests/unit/ + nosetests --with-timer --rednose -sv st2reactor/tests/integration/ nosetests --with-timer --rednose -sv --ignore-files=test_kvps.* st2api/tests/unit/controllers/v1/ nosetests --with-timer --rednose -sv --ignore-files=test_validator_mistral.* st2api/tests/unit/controllers/exp/ nosetests --with-timer --rednose -sv --ignore-files=test_jinja_render_crypto_filters.* --ignore-files=test_config_loader.* --ignore-files=test_crypto_utils.* --ignore-files=test_db.* --ignore-files=test_logging.* st2common/tests/unit/ From 4d7b4264238006f8d17382253301c2cb08df7be0 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 20:57:37 +0200 Subject: [PATCH 14/40] Use tox 3.0.0. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5592cc27a5..99ae2feda0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ before_install: - sudo pip install --upgrade "virtualenv==15.1.0" install: - - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install tox; else make requirements; fi + - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install "tox==3.0.0"; else make requirements; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ]; then pip install codecov; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ] || [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then sudo .circle/add-itest-user.sh; fi From 476973ac0dfeac25be1a0077ad38aead0fb56474 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 21:01:47 +0200 Subject: [PATCH 15/40] Print tox version. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99ae2feda0..1b0b57a46c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ before_install: - sudo pip install --upgrade "virtualenv==15.1.0" install: - - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install "tox==3.0.0"; else make requirements; fi + - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install "tox==3.0.0" && tox --version; else make requirements; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ]; then pip install codecov; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ] || [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then sudo .circle/add-itest-user.sh; fi From 9c11d0117669471a52c08b27b058b57e5c72a91e Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 21:11:19 +0200 Subject: [PATCH 16/40] Remove step we dont need. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1b0b57a46c..99ae2feda0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ before_install: - sudo pip install --upgrade "virtualenv==15.1.0" install: - - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install "tox==3.0.0" && tox --version; else make requirements; fi + - if [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then pip install "tox==3.0.0"; else make requirements; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ]; then pip install codecov; fi - if [ "${TASK}" = 'ci-unit' ] || [ "${TASK}" = 'ci-integration' ] || [ "${TASK}" = 'compilepy3 ci-py3-unit' ]; then sudo .circle/add-itest-user.sh; fi From 0bf867e051bb3e870ecc01b910dba9769df62fc8 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 21:49:29 +0200 Subject: [PATCH 17/40] Increase sleep. --- st2reactor/tests/integration/test_garbage_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2reactor/tests/integration/test_garbage_collector.py b/st2reactor/tests/integration/test_garbage_collector.py index 618ec238ac..18091e851e 100644 --- a/st2reactor/tests/integration/test_garbage_collector.py +++ b/st2reactor/tests/integration/test_garbage_collector.py @@ -180,7 +180,7 @@ def test_garbage_collection(self): process = self._start_garbage_collector() # Give it some time to perform garbage collection and kill it - eventlet.sleep(8) + eventlet.sleep(10) process.send_signal(signal.SIGKILL) self.remove_process(process=process) From 488bc50f4ab69da7ebc9a889ce7494834a42bfbe Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 18 May 2018 22:21:34 +0200 Subject: [PATCH 18/40] For now ignore GC tests. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d8add89eb5..6a3def8e9e 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ deps = virtualenv commands = nosetests --with-timer --rednose -sv st2client/tests/unit/ nosetests --with-timer --rednose -sv st2reactor/tests/unit/ - nosetests --with-timer --rednose -sv st2reactor/tests/integration/ + nosetests --with-timer --rednose -sv st2reactor/tests/integration/ --ignore-files=test_garbage_collector.* nosetests --with-timer --rednose -sv --ignore-files=test_kvps.* st2api/tests/unit/controllers/v1/ nosetests --with-timer --rednose -sv --ignore-files=test_validator_mistral.* st2api/tests/unit/controllers/exp/ nosetests --with-timer --rednose -sv --ignore-files=test_jinja_render_crypto_filters.* --ignore-files=test_config_loader.* --ignore-files=test_crypto_utils.* --ignore-files=test_db.* --ignore-files=test_logging.* st2common/tests/unit/ From d2774ee8ece2e91a38c8c3e92656bc7b7fc49610 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 11:05:00 +0200 Subject: [PATCH 19/40] Add missing __all__, expose SUPPORTED_FILTERS dict. --- st2api/st2api/controllers/v1/rule_enforcements.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/st2api/st2api/controllers/v1/rule_enforcements.py b/st2api/st2api/controllers/v1/rule_enforcements.py index 687a3b9eaa..425fe11581 100644 --- a/st2api/st2api/controllers/v1/rule_enforcements.py +++ b/st2api/st2api/controllers/v1/rule_enforcements.py @@ -21,7 +21,12 @@ from st2common.util import isotime from st2common.rbac.types import PermissionType -from st2api.controllers import resource +from st2api.controllers.resource import ResourceController + +__all__ = [ + 'RuleEnforcementController', + 'SUPPORTED_FILTERS' +] http_client = six.moves.http_client @@ -40,7 +45,7 @@ } -class RuleEnforcementController(resource.ResourceController): +class RuleEnforcementController(ResourceController): model = RuleEnforcementAPI access = RuleEnforcement From 7032bfedfd85fe1224b4413a19d038d350a10fd9 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 11:07:00 +0200 Subject: [PATCH 20/40] Also expose some other constants so they can be re-used inside a new views controller. --- .../controllers/v1/rule_enforcements.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/st2api/st2api/controllers/v1/rule_enforcements.py b/st2api/st2api/controllers/v1/rule_enforcements.py index 425fe11581..47fc6be1f0 100644 --- a/st2api/st2api/controllers/v1/rule_enforcements.py +++ b/st2api/st2api/controllers/v1/rule_enforcements.py @@ -25,7 +25,10 @@ __all__ = [ 'RuleEnforcementController', - 'SUPPORTED_FILTERS' + + 'SUPPORTED_FILTERS', + 'QUERY_OPTIONS', + 'FILTER_TRANSFORM_FUNCTIONS' ] @@ -44,6 +47,16 @@ 'enforced_at_lt': 'enforced_at.lt' } +QUERY_OPTIONS = { + 'sort': ['-enforced_at', 'rule.ref'] +} + +FILTER_TRANSFORM_FUNCTIONS = { + 'enforced_at': lambda value: isotime.parse(value=value), + 'enforced_at_gt': lambda value: isotime.parse(value=value), + 'enforced_at_lt': lambda value: isotime.parse(value=value) +} + class RuleEnforcementController(ResourceController): @@ -51,16 +64,10 @@ class RuleEnforcementController(ResourceController): access = RuleEnforcement # ResourceController attributes - query_options = { - 'sort': ['-enforced_at', 'rule.ref'] - } + query_options = QUERY_OPTIONS supported_filters = SUPPORTED_FILTERS - filter_transform_functions = { - 'enforced_at': lambda value: isotime.parse(value=value), - 'enforced_at_gt': lambda value: isotime.parse(value=value), - 'enforced_at_lt': lambda value: isotime.parse(value=value) - } + filter_transform_functions = FILTER_TRANSFORM_FUNCTIONS def get_all(self, sort=None, offset=0, limit=None, requester_user=None, **raw_filters): return super(RuleEnforcementController, self)._get_all(sort=sort, From 6fd512f5b3800248071586fa4a8521e177363a5d Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 11:36:23 +0200 Subject: [PATCH 21/40] Fix MongoDB profiling util and make sure document fields are correctly quoted when projection is used on a query (aka only want to retrieve a subset of fields or similar). --- st2common/st2common/models/utils/profiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2common/st2common/models/utils/profiling.py b/st2common/st2common/models/utils/profiling.py index b4e8482bab..54a66d0c2b 100644 --- a/st2common/st2common/models/utils/profiling.py +++ b/st2common/st2common/models/utils/profiling.py @@ -116,7 +116,7 @@ def construct_mongo_shell_query(mongo_query, collection_name, ordering, limit, # Include only fields (projection) if only_fields: - projection_items = ['%s: 1' % (field) for field in only_fields] + projection_items = ['\'%s\': 1' % (field) for field in only_fields] projection = ', '.join(projection_items) part = 'find({filter_predicate}, {{{projection}}})'.format( filter_predicate=filter_predicate, projection=projection) From 9bc8a3ecbf79976b6be3eb8b029b30bb134be14a Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 12:06:02 +0200 Subject: [PATCH 22/40] Add new /v1/ruleenforcements/views[/] API endpoint which returns rule enforcement objects enriched with additional data useful to the WebUI and various clients (right now that's execution parameters and action ref). --- .../controllers/v1/rule_enforcement_views.py | 104 +++++++++++++++++ .../st2common/models/api/rule_enforcement.py | 22 +++- st2common/st2common/openapi.yaml | 109 ++++++++++++++++++ st2common/st2common/openapi.yaml.j2 | 109 ++++++++++++++++++ 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 st2api/st2api/controllers/v1/rule_enforcement_views.py diff --git a/st2api/st2api/controllers/v1/rule_enforcement_views.py b/st2api/st2api/controllers/v1/rule_enforcement_views.py new file mode 100644 index 0000000000..733dd51ff9 --- /dev/null +++ b/st2api/st2api/controllers/v1/rule_enforcement_views.py @@ -0,0 +1,104 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from st2common.models.api.rule_enforcement import RuleEnforcementViewAPI +from st2common.persistence.rule_enforcement import RuleEnforcement +from st2common.persistence.execution import ActionExecution +from st2api.controllers.v1.rule_enforcements import SUPPORTED_FILTERS +from st2api.controllers.v1.rule_enforcements import QUERY_OPTIONS +from st2api.controllers.v1.rule_enforcements import FILTER_TRANSFORM_FUNCTIONS +from st2common.rbac.types import PermissionType + +from st2api.controllers.resource import ResourceController + +__all__ = [ + 'RuleEnforcementViewController' +] + + +class RuleEnforcementViewController(ResourceController): + """ + API controller which adds some extra information to the rule enforcement object so it makes + more efficient for UI and clients to render rule enforcement object. + + Right now in case a trigger instance matched an execution and execution was triggered, it also + includes action input parameters for the trigger action. + """ + + model = RuleEnforcementViewAPI + access = RuleEnforcement + + query_options = QUERY_OPTIONS + + supported_filters = SUPPORTED_FILTERS + filter_transform_functions = FILTER_TRANSFORM_FUNCTIONS + + def get_all(self, sort=None, offset=0, limit=None, requester_user=None, **raw_filters): + rule_enforcement_apis = super(RuleEnforcementViewController, self)._get_all(sort=sort, + offset=offset, + limit=limit, + raw_filters=raw_filters, + requester_user=requester_user) + + rule_enforcement_apis.json = self._append_view_properties(rule_enforcement_apis.json) + return rule_enforcement_apis + + def get_one(self, id, requester_user): + rule_enforcement_api = super(RuleEnforcementViewController, + self)._get_one_by_id(id, requester_user=requester_user, + permission_type=PermissionType.RULE_ENFORCEMENT_VIEW) + rule_enforcement_api = self._append_view_properties([rule_enforcement_api.__json__()])[0] + return rule_enforcement_api + + def _append_view_properties(self, rule_enforcement_apis): + execution_ids = [] + + for rule_enforcement_api in rule_enforcement_apis: + if rule_enforcement_api.get('execution_id', None): + execution_ids.append(rule_enforcement_api['execution_id']) + + # NOTE: Executions contain a lot of field and could contain a lot of data so we only + # retrieve fields we need + execution_dbs = ActionExecution.query(id__in=execution_ids, + only_fields=['id', 'action.ref', 'parameters']) + execution_dbs_by_id = {} + + for execution_db in execution_dbs: + execution_dbs_by_id[str(execution_db.id)] = execution_db + + # Ammend rule enforcement objects with additional data + for rule_enforcement_api in rule_enforcement_apis: + rule_enforcement_api['execution'] = {} + execution_id = rule_enforcement_api.get('execution_id', None) + + if not execution_id: + continue + + execution_db = execution_dbs_by_id.get(execution_id, None) + + if not execution_db: + continue + + rule_enforcement_api['execution'] = { + 'action': { + 'ref': execution_db['action']['ref'] + }, + 'parameters': execution_db['parameters'] + } + + return rule_enforcement_apis + + +rule_enforcement_view_controller = RuleEnforcementViewController() diff --git a/st2common/st2common/models/api/rule_enforcement.py b/st2common/st2common/models/api/rule_enforcement.py index 166b2448c5..d235cad6e3 100644 --- a/st2common/st2common/models/api/rule_enforcement.py +++ b/st2common/st2common/models/api/rule_enforcement.py @@ -14,12 +14,24 @@ # limitations under the License. from __future__ import absolute_import + +import copy + import six from st2common.models.api.base import BaseAPI -from st2common.models.db.rule_enforcement import RuleEnforcementDB, RuleReferenceSpecDB +from st2common.models.db.rule_enforcement import RuleEnforcementDB +from st2common.models.db.rule_enforcement import RuleReferenceSpecDB +from st2common.models.api.execution import ActionExecutionAPI from st2common.util import isotime +__all__ = [ + 'RuleEnforcementAPI', + 'RuleEnforcementViewAPI', + + 'RuleReferenceSpecDB' +] + class RuleReferenceSpec(BaseAPI): schema = { @@ -97,3 +109,11 @@ def from_model(cls, model, mask_secrets=False): doc['enforced_at'] = enforced_at attrs = {attr: value for attr, value in six.iteritems(doc) if value} return cls(**attrs) + + +class RuleEnforcementViewAPI(RuleEnforcementAPI): + # Always deep-copy to avoid breaking the original. + schema = copy.deepcopy(RuleEnforcementAPI.schema) + + # Update the schema to include additional execution properties + schema['properties']['execution'] = copy.deepcopy(ActionExecutionAPI.schema) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index d949403b62..270e00ce60 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -3342,6 +3342,115 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + /api/v1/ruleenforcements/views: + get: + operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all + x-permissions: rule_enforcement_list + description: Returns a list of all the rule enforcements. + parameters: + - name: limit + in: query + description: List N most recent rule enforcements. + type: integer + default: 50 + - name: offset + in: query + description: Number of rule enforcements to offset + type: integer + default: 0 + - name: sort + in: query + description: Comma-separated list of fields to sort by + type: string + - name: id + in: query + description: Entity id filter + type: array + items: + type: string + - name: name + in: query + description: Entity name filter + type: array + items: + type: string + - name: execution + in: query + description: Execution to filter the list. + type: string + - name: rule_ref + in: query + description: Rule ref to filter the list. + type: string + - name: rule_id + in: query + description: Rule id to filter the list.. + type: string + - name: trigger_instance + in: query + description: Trace instance id to filter the list. + type: string + - name: enforced_at_gt + in: query + description: | + Only return enforcements with enforced_at greater than the one provided. Use time in the format. + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ + - name: enforced_at_lt + in: query + description: | + Only return enforcements with enforced_at lower than the one provided. Use time in the format. + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s|T)\d{2}:\d{2}:\d{2}(\.\d{3,6})?(Z|\+00|\+0000|\+00:00)$ + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: List of rule enforcements + schema: + type: array + items: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /api/v1/ruleenforcements/views/{id}: + get: + operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one + description: Return a specific rule enforcement based on id. + parameters: + - name: id + in: path + description: Entity id + type: string + required: true + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: Rule Enforcements based on ref or id + schema: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /api/v1/timers: get: operationId: st2api.controllers.v1.timers:timers_controller.get_all diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 458359f005..853afd0b51 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -3338,6 +3338,115 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + /api/v1/ruleenforcements/views: + get: + operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all + x-permissions: {{ PERMISSION_TYPE.RULE_ENFORCEMENT_LIST }} + description: Returns a list of all the rule enforcements. + parameters: + - name: limit + in: query + description: List N most recent rule enforcements. + type: integer + default: 50 + - name: offset + in: query + description: Number of rule enforcements to offset + type: integer + default: 0 + - name: sort + in: query + description: Comma-separated list of fields to sort by + type: string + - name: id + in: query + description: Entity id filter + type: array + items: + type: string + - name: name + in: query + description: Entity name filter + type: array + items: + type: string + - name: execution + in: query + description: Execution to filter the list. + type: string + - name: rule_ref + in: query + description: Rule ref to filter the list. + type: string + - name: rule_id + in: query + description: Rule id to filter the list.. + type: string + - name: trigger_instance + in: query + description: Trace instance id to filter the list. + type: string + - name: enforced_at_gt + in: query + description: | + Only return enforcements with enforced_at greater than the one provided. Use time in the format. + type: string + pattern: {{ ISO8601_UTC_REGEX }} + - name: enforced_at_lt + in: query + description: | + Only return enforcements with enforced_at lower than the one provided. Use time in the format. + type: string + pattern: {{ ISO8601_UTC_REGEX }} + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: List of rule enforcements + schema: + type: array + items: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /api/v1/ruleenforcements/views/{id}: + get: + operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one + description: Return a specific rule enforcement based on id. + parameters: + - name: id + in: path + description: Entity id + type: string + required: true + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: Rule Enforcements based on ref or id + schema: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /api/v1/timers: get: operationId: st2api.controllers.v1.timers:timers_controller.get_all From e0936563656ea9f5acd38a140c0047c321ccf071 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 12:33:54 +0200 Subject: [PATCH 23/40] Update rule enforcements views API endpoint to also include corresponding trigger instance object. --- .../controllers/v1/rule_enforcement_views.py | 53 +++++++++++++------ .../st2common/models/api/rule_enforcement.py | 4 ++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/st2api/st2api/controllers/v1/rule_enforcement_views.py b/st2api/st2api/controllers/v1/rule_enforcement_views.py index 733dd51ff9..896c7435fc 100644 --- a/st2api/st2api/controllers/v1/rule_enforcement_views.py +++ b/st2api/st2api/controllers/v1/rule_enforcement_views.py @@ -14,8 +14,10 @@ # limitations under the License. from st2common.models.api.rule_enforcement import RuleEnforcementViewAPI +from st2common.models.api.trigger import TriggerInstanceAPI from st2common.persistence.rule_enforcement import RuleEnforcement from st2common.persistence.execution import ActionExecution +from st2common.persistence.trigger import TriggerInstance from st2api.controllers.v1.rule_enforcements import SUPPORTED_FILTERS from st2api.controllers.v1.rule_enforcements import QUERY_OPTIONS from st2api.controllers.v1.rule_enforcements import FILTER_TRANSFORM_FUNCTIONS @@ -33,8 +35,10 @@ class RuleEnforcementViewController(ResourceController): API controller which adds some extra information to the rule enforcement object so it makes more efficient for UI and clients to render rule enforcement object. - Right now in case a trigger instance matched an execution and execution was triggered, it also - includes action input parameters for the trigger action. + Right now we include those fields: + + * trigger_instance object for each rule enforcement object + * subset of an execution object in case execution was triggered """ model = RuleEnforcementViewAPI @@ -63,40 +67,59 @@ def get_one(self, id, requester_user): return rule_enforcement_api def _append_view_properties(self, rule_enforcement_apis): + """ + Method which appends corresponding execution (if available) and trigger instance object + properties. + """ + trigger_instance_ids = set([]) execution_ids = [] for rule_enforcement_api in rule_enforcement_apis: + trigger_instance_ids.add(str(rule_enforcement_api['trigger_instance_id'])) + if rule_enforcement_api.get('execution_id', None): execution_ids.append(rule_enforcement_api['execution_id']) + # 1. Retrieve corresponding execution objects # NOTE: Executions contain a lot of field and could contain a lot of data so we only # retrieve fields we need execution_dbs = ActionExecution.query(id__in=execution_ids, only_fields=['id', 'action.ref', 'parameters']) - execution_dbs_by_id = {} + execution_dbs_by_id = {} for execution_db in execution_dbs: execution_dbs_by_id[str(execution_db.id)] = execution_db + # 2. Retrieve corresponding trigger instance objects + trigger_instance_dbs = TriggerInstance.query(id__in=list(trigger_instance_ids)) + + trigger_instance_dbs_by_id = {} + + for trigger_instance_db in trigger_instance_dbs: + trigger_instance_dbs_by_id[str(trigger_instance_db.id)] = trigger_instance_db + # Ammend rule enforcement objects with additional data for rule_enforcement_api in rule_enforcement_apis: + rule_enforcement_api['trigger_instance'] = {} rule_enforcement_api['execution'] = {} - execution_id = rule_enforcement_api.get('execution_id', None) - if not execution_id: - continue + trigger_instance_id = rule_enforcement_api['trigger_instance_id'] + execution_id = rule_enforcement_api.get('execution_id', None) + trigger_instance_db = trigger_instance_dbs_by_id.get(trigger_instance_id, None) execution_db = execution_dbs_by_id.get(execution_id, None) - if not execution_db: - continue - - rule_enforcement_api['execution'] = { - 'action': { - 'ref': execution_db['action']['ref'] - }, - 'parameters': execution_db['parameters'] - } + if trigger_instance_db: + trigger_instance_api = TriggerInstanceAPI.from_model(trigger_instance_db) + rule_enforcement_api['trigger_instance'] = trigger_instance_api + + if execution_db: + rule_enforcement_api['execution'] = { + 'action': { + 'ref': execution_db['action']['ref'] + }, + 'parameters': execution_db['parameters'] + } return rule_enforcement_apis diff --git a/st2common/st2common/models/api/rule_enforcement.py b/st2common/st2common/models/api/rule_enforcement.py index d235cad6e3..cc95b05514 100644 --- a/st2common/st2common/models/api/rule_enforcement.py +++ b/st2common/st2common/models/api/rule_enforcement.py @@ -23,6 +23,7 @@ from st2common.models.db.rule_enforcement import RuleEnforcementDB from st2common.models.db.rule_enforcement import RuleReferenceSpecDB from st2common.models.api.execution import ActionExecutionAPI +from st2common.models.api.trigger import TriggerInstanceAPI from st2common.util import isotime __all__ = [ @@ -117,3 +118,6 @@ class RuleEnforcementViewAPI(RuleEnforcementAPI): # Update the schema to include additional execution properties schema['properties']['execution'] = copy.deepcopy(ActionExecutionAPI.schema) + + # Update the schema to include additional trigger instance properties + schema['properties']['trigger_instance'] = copy.deepcopy(TriggerInstanceAPI.schema) From 24b5b663c6c3328fdbfaecedd980858308c3c04b Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 16:30:14 +0200 Subject: [PATCH 24/40] Add changelog entry. --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71bdf7e8e3..4030563aa9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,9 @@ Added This change increases visibility into rules which didn't match due to an exception. Previously this was only visible / reflected in the rules engine log file. (improvement) #4134 +* Add new ``GET /v1/ruleenforcements/views[/]`` API endpoints which allow user to + retrieve RuleEnforcement objects with the corresponding TriggerInstance and Execution objects. + (new feature) #4134 Changed ~~~~~~~ From da163e17061364995eb6076e3d89ec640857c882 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Tue, 22 May 2018 17:23:36 +0200 Subject: [PATCH 25/40] Use consistent module name - separate words with underscore. --- st2api/st2api/controllers/exp/inquiries.py | 2 +- .../v1/{actionviews.py => action_views.py} | 0 st2api/st2api/controllers/v1/actionexecutions.py | 4 ++-- st2api/st2api/controllers/v1/actions.py | 2 +- .../v1/{executionviews.py => execution_views.py} | 0 .../controllers/v1/{packviews.py => pack_views.py} | 0 st2api/st2api/controllers/v1/packs.py | 2 +- .../controllers/v1/{ruleviews.py => rule_views.py} | 0 st2api/st2api/controllers/v1/rules.py | 2 +- .../unit/controllers/v1/test_executions_filters.py | 2 +- .../tests/unit/controllers/v1/test_packs_views.py | 2 +- st2common/st2common/openapi.yaml | 14 +++++++------- st2common/st2common/openapi.yaml.j2 | 14 +++++++------- 13 files changed, 22 insertions(+), 22 deletions(-) rename st2api/st2api/controllers/v1/{actionviews.py => action_views.py} (100%) rename st2api/st2api/controllers/v1/{executionviews.py => execution_views.py} (100%) rename st2api/st2api/controllers/v1/{packviews.py => pack_views.py} (100%) rename st2api/st2api/controllers/v1/{ruleviews.py => rule_views.py} (100%) diff --git a/st2api/st2api/controllers/exp/inquiries.py b/st2api/st2api/controllers/exp/inquiries.py index d09fa1d8bd..9462c58624 100644 --- a/st2api/st2api/controllers/exp/inquiries.py +++ b/st2api/st2api/controllers/exp/inquiries.py @@ -21,7 +21,7 @@ from six.moves import http_client from st2common.models.db.auth import UserDB from st2api.controllers.resource import ResourceController -from st2api.controllers.v1.executionviews import SUPPORTED_FILTERS +from st2api.controllers.v1.execution_views import SUPPORTED_FILTERS from st2common import log as logging from st2common.util import action_db as action_utils from st2common.util import schema as util_schema diff --git a/st2api/st2api/controllers/v1/actionviews.py b/st2api/st2api/controllers/v1/action_views.py similarity index 100% rename from st2api/st2api/controllers/v1/actionviews.py rename to st2api/st2api/controllers/v1/action_views.py diff --git a/st2api/st2api/controllers/v1/actionexecutions.py b/st2api/st2api/controllers/v1/actionexecutions.py index c89323110b..4eed1b820e 100644 --- a/st2api/st2api/controllers/v1/actionexecutions.py +++ b/st2api/st2api/controllers/v1/actionexecutions.py @@ -27,8 +27,8 @@ from st2api.controllers.base import BaseRestControllerMixin from st2api.controllers.resource import ResourceController from st2api.controllers.resource import BaseResourceIsolationControllerMixin -from st2api.controllers.v1.executionviews import ExecutionViewsController -from st2api.controllers.v1.executionviews import SUPPORTED_FILTERS +from st2api.controllers.v1.execution_views import ExecutionViewsController +from st2api.controllers.v1.execution_views import SUPPORTED_FILTERS from st2common import log as logging from st2common.constants import action as action_constants from st2common.exceptions import actionrunner as runner_exc diff --git a/st2api/st2api/controllers/v1/actions.py b/st2api/st2api/controllers/v1/actions.py index 8228e451ea..592b70a037 100644 --- a/st2api/st2api/controllers/v1/actions.py +++ b/st2api/st2api/controllers/v1/actions.py @@ -24,7 +24,7 @@ # StackStorm defined exceptions. from st2api.controllers import resource -from st2api.controllers.v1.actionviews import ActionViewsController +from st2api.controllers.v1.action_views import ActionViewsController from st2common import log as logging from st2common.constants.triggers import ACTION_FILE_WRITTEN_TRIGGER from st2common.exceptions.action import InvalidActionParameterException diff --git a/st2api/st2api/controllers/v1/executionviews.py b/st2api/st2api/controllers/v1/execution_views.py similarity index 100% rename from st2api/st2api/controllers/v1/executionviews.py rename to st2api/st2api/controllers/v1/execution_views.py diff --git a/st2api/st2api/controllers/v1/packviews.py b/st2api/st2api/controllers/v1/pack_views.py similarity index 100% rename from st2api/st2api/controllers/v1/packviews.py rename to st2api/st2api/controllers/v1/pack_views.py diff --git a/st2api/st2api/controllers/v1/packs.py b/st2api/st2api/controllers/v1/packs.py index 0765193f2e..e36dedbbb7 100644 --- a/st2api/st2api/controllers/v1/packs.py +++ b/st2api/st2api/controllers/v1/packs.py @@ -314,7 +314,7 @@ def get_all(self): class PacksController(BasePacksController): - from st2api.controllers.v1.packviews import PackViewsController + from st2api.controllers.v1.pack_views import PackViewsController model = PackAPI access = Pack diff --git a/st2api/st2api/controllers/v1/ruleviews.py b/st2api/st2api/controllers/v1/rule_views.py similarity index 100% rename from st2api/st2api/controllers/v1/ruleviews.py rename to st2api/st2api/controllers/v1/rule_views.py diff --git a/st2api/st2api/controllers/v1/rules.py b/st2api/st2api/controllers/v1/rules.py index 4c9f0c0f0f..5a74ae25e4 100644 --- a/st2api/st2api/controllers/v1/rules.py +++ b/st2api/st2api/controllers/v1/rules.py @@ -24,7 +24,7 @@ from st2api.controllers.resource import BaseResourceIsolationControllerMixin from st2api.controllers.resource import ContentPackResourceController from st2api.controllers.controller_transforms import transform_to_bool -from st2api.controllers.v1.ruleviews import RuleViewController +from st2api.controllers.v1.rule_views import RuleViewController from st2common.models.api.rule import RuleAPI from st2common.models.db.auth import UserDB from st2common.persistence.rule import Rule diff --git a/st2api/tests/unit/controllers/v1/test_executions_filters.py b/st2api/tests/unit/controllers/v1/test_executions_filters.py index d5874240b6..1ce3b7633e 100644 --- a/st2api/tests/unit/controllers/v1/test_executions_filters.py +++ b/st2api/tests/unit/controllers/v1/test_executions_filters.py @@ -30,7 +30,7 @@ from st2common.util import isotime from st2common.util import date as date_utils from st2api.controllers.v1.actionexecutions import ActionExecutionsController -from st2api.controllers.v1.executionviews import FILTERS_WITH_VALID_NULL_VALUES +from st2api.controllers.v1.execution_views import FILTERS_WITH_VALID_NULL_VALUES from st2common.persistence.execution import ActionExecution from st2common.models.api.execution import ActionExecutionAPI diff --git a/st2api/tests/unit/controllers/v1/test_packs_views.py b/st2api/tests/unit/controllers/v1/test_packs_views.py index 1b801755e5..e3b6ff5d07 100644 --- a/st2api/tests/unit/controllers/v1/test_packs_views.py +++ b/st2api/tests/unit/controllers/v1/test_packs_views.py @@ -77,7 +77,7 @@ def test_get_pack_file_pack_doesnt_exist(self): resp = self.app.get('/v1/packs/views/files/doesntexist/pack.yaml', expect_errors=True) self.assertEqual(resp.status_int, http_client.NOT_FOUND) - @mock.patch('st2api.controllers.v1.packviews.MAX_FILE_SIZE', 1) + @mock.patch('st2api.controllers.v1.pack_views.MAX_FILE_SIZE', 1) def test_pack_file_file_larger_then_maximum_size(self): resp = self.app.get('/v1/packs/views/file/dummy_pack_1/pack.yaml', expect_errors=True) self.assertEqual(resp.status_int, http_client.BAD_REQUEST) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 270e00ce60..5679723995 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -422,7 +422,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/parameters/{action_id}: get: - operationId: st2api.controllers.v1.actionviews:parameters_view_controller.get_one + operationId: st2api.controllers.v1.action_views:parameters_view_controller.get_one description: | Get parameters for an action. parameters: @@ -453,7 +453,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/overview: get: - operationId: st2api.controllers.v1.actionviews:overview_controller.get_all + operationId: st2api.controllers.v1.action_views:overview_controller.get_all x-permissions: action_list description: Returns a list of all the actions with runner parameters included. parameters: @@ -514,7 +514,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/overview/{ref_or_id}: get: - operationId: st2api.controllers.v1.actionviews:overview_controller.get_one + operationId: st2api.controllers.v1.action_views:overview_controller.get_one description: | Get one action with runner parameters included. parameters: @@ -543,7 +543,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/entry_point/{ref_or_id}: get: - operationId: st2api.controllers.v1.actionviews:entry_point_controller.get_one + operationId: st2api.controllers.v1.action_views:entry_point_controller.get_one description: | Get code of the action's entry_point. parameters: @@ -1305,7 +1305,7 @@ paths: $ref: '#/definitions/Error' /api/v1/executions/views/filters: get: - operationId: st2api.controllers.v1.executionviews:filters_controller.get_all + operationId: st2api.controllers.v1.execution_views:filters_controller.get_all x-permissions: execution_views_filters_list description: | Get a list of distinct values for the execution filters. @@ -2690,7 +2690,7 @@ paths: $ref: '#/definitions/Error' /api/v1/rules/views: get: - operationId: st2api.controllers.v1.ruleviews:rule_view_controller.get_all + operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_all x-permissions: rule_list description: Returns a list of all rules. parameters: @@ -2760,7 +2760,7 @@ paths: $ref: '#/definitions/Error' /api/v1/rules/views/{ref_or_id}: get: - operationId: st2api.controllers.v1.ruleviews:rule_view_controller.get_one + operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_one description: | Get one rule. parameters: diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 853afd0b51..7a4ba0742c 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -418,7 +418,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/parameters/{action_id}: get: - operationId: st2api.controllers.v1.actionviews:parameters_view_controller.get_one + operationId: st2api.controllers.v1.action_views:parameters_view_controller.get_one description: | Get parameters for an action. parameters: @@ -449,7 +449,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/overview: get: - operationId: st2api.controllers.v1.actionviews:overview_controller.get_all + operationId: st2api.controllers.v1.action_views:overview_controller.get_all x-permissions: {{ PERMISSION_TYPE.ACTION_LIST }} description: Returns a list of all the actions with runner parameters included. parameters: @@ -510,7 +510,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/overview/{ref_or_id}: get: - operationId: st2api.controllers.v1.actionviews:overview_controller.get_one + operationId: st2api.controllers.v1.action_views:overview_controller.get_one description: | Get one action with runner parameters included. parameters: @@ -539,7 +539,7 @@ paths: $ref: '#/definitions/Error' /api/v1/actions/views/entry_point/{ref_or_id}: get: - operationId: st2api.controllers.v1.actionviews:entry_point_controller.get_one + operationId: st2api.controllers.v1.action_views:entry_point_controller.get_one description: | Get code of the action's entry_point. parameters: @@ -1301,7 +1301,7 @@ paths: $ref: '#/definitions/Error' /api/v1/executions/views/filters: get: - operationId: st2api.controllers.v1.executionviews:filters_controller.get_all + operationId: st2api.controllers.v1.execution_views:filters_controller.get_all x-permissions: {{ PERMISSION_TYPE.EXECUTION_VIEWS_FILTERS_LIST }} description: | Get a list of distinct values for the execution filters. @@ -2686,7 +2686,7 @@ paths: $ref: '#/definitions/Error' /api/v1/rules/views: get: - operationId: st2api.controllers.v1.ruleviews:rule_view_controller.get_all + operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_all x-permissions: {{ PERMISSION_TYPE.RULE_LIST }} description: Returns a list of all rules. parameters: @@ -2756,7 +2756,7 @@ paths: $ref: '#/definitions/Error' /api/v1/rules/views/{ref_or_id}: get: - operationId: st2api.controllers.v1.ruleviews:rule_view_controller.get_one + operationId: st2api.controllers.v1.rule_views:rule_view_controller.get_one description: | Get one rule. parameters: From 0d8786ee283c73e95928d84621929f0e4fe0a8f4 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 10:10:21 +0200 Subject: [PATCH 26/40] Add use_object_ids argument to the fixtures loader. When this argument is True, we use object ids (primary keys) from fixture files when saving objects in the database. This allows us to re-create reproducible tests in cases when tests rely on fixed ids without needing to mock a lot of things (mocking is error prone and not robust in a lot of scenarios). --- st2tests/st2tests/fixturesloader.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/st2tests/st2tests/fixturesloader.py b/st2tests/st2tests/fixturesloader.py index 57c3504ca2..bdde627886 100644 --- a/st2tests/st2tests/fixturesloader.py +++ b/st2tests/st2tests/fixturesloader.py @@ -155,7 +155,8 @@ class FixturesLoader(object): def __init__(self): self.meta_loader = MetaLoader() - def save_fixtures_to_db(self, fixtures_pack='generic', fixtures_dict=None): + def save_fixtures_to_db(self, fixtures_pack='generic', fixtures_dict=None, + use_object_ids=False): """ Loads fixtures specified in fixtures_dict into the database and returns DB models for the fixtures. @@ -173,10 +174,17 @@ def save_fixtures_to_db(self, fixtures_pack='generic', fixtures_dict=None): :param fixtures_dict: Dictionary specifying the fixtures to load for each type. :type fixtures_dict: ``dict`` + :param use_object_ids: Use object id primary key from fixture file (if available) when + storing objects in the database. By default id in + file is discarded / not used and a new random one + is generated. + :type use_object_ids: ``bool`` + :rtype: ``dict`` """ if fixtures_dict is None: fixtures_dict = {} + fixtures_pack_path = self._validate_fixtures_pack(fixtures_pack) self._validate_fixture_dict(fixtures_dict, allowed=ALLOWED_DB_FIXTURES) @@ -196,6 +204,11 @@ def save_fixtures_to_db(self, fixtures_pack='generic', fixtures_dict=None): self._get_fixture_file_path_abs(fixtures_pack_path, fixture_type, fixture)) api_model = API_MODEL(**fixture_dict) db_model = API_MODEL.to_model(api_model) + + # Make sure we also set and use object id if that functionality is used + if use_object_ids and 'id' in fixture_dict: + db_model.id = fixture_dict['id'] + db_model = PERSISTENCE_MODEL.add_or_update(db_model) loaded_fixtures[fixture] = db_model From 45dde7f8c4f0f1261bb3676a0c83e40af781a403 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 10:29:03 +0200 Subject: [PATCH 27/40] Add test cases for rule enforcement views API endpoint. --- .../v1/test_rule_enforcement_views.py | 80 +++++++++++++++++++ .../executions/execution1.yaml | 38 +++++++++ .../triggerinstances/trigger_instance_1.yaml | 8 ++ 3 files changed, 126 insertions(+) create mode 100644 st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py create mode 100644 st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml create mode 100644 st2tests/st2tests/fixtures/rule_enforcements/triggerinstances/trigger_instance_1.yaml diff --git a/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py new file mode 100644 index 0000000000..eccb528974 --- /dev/null +++ b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py @@ -0,0 +1,80 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import six + +from st2tests.fixturesloader import FixturesLoader +from tests import FunctionalTest + +__all__ = [ + 'RuleEnforcementViewsControllerTestCase' +] + +http_client = six.moves.http_client + +TEST_FIXTURES = { + 'enforcements': ['enforcement1.yaml', 'enforcement2.yaml', 'enforcement3.yaml'], + 'executions': ['execution1.yaml'], + 'triggerinstances': ['trigger_instance_1.yaml'] +} + +FIXTURES_PACK = 'rule_enforcements' + + +class RuleEnforcementViewsControllerTestCase(FunctionalTest): + + fixtures_loader = FixturesLoader() + + @classmethod + def setUpClass(cls): + super(RuleEnforcementViewsControllerTestCase, cls).setUpClass() + models = RuleEnforcementViewsControllerTestCase.fixtures_loader.save_fixtures_to_db( + fixtures_pack=FIXTURES_PACK, fixtures_dict=TEST_FIXTURES, + use_object_ids=True) + cls.ENFORCEMENT_1 = models['enforcements']['enforcement1.yaml'] + + def test_get_all(self): + resp = self.app.get('/v1/ruleenforcements/views') + self.assertEqual(resp.status_int, http_client.OK) + self.assertEqual(len(resp.json), 3) + + # Verify it includes corresponding execution and trigger instance object + self.assertEqual(resp.json[0]['trigger_instance']['id'], '565e15ce32ed350857dfa623') + self.assertEqual(resp.json[0]['trigger_instance']['payload'], {'foo': 'bar', 'name': 'Joe'}) + + self.assertEqual(resp.json[0]['execution']['action']['ref'], 'core.local') + self.assertEqual(resp.json[0]['execution']['parameters'], {'cmd': 'echo bar'}) + + self.assertEqual(resp.json[1]['trigger_instance'], {}) + self.assertEqual(resp.json[1]['execution'], {}) + + self.assertEqual(resp.json[2]['trigger_instance'], {}) + self.assertEqual(resp.json[2]['execution'], {}) + + def test_filter_by_rule_ref(self): + resp = self.app.get('/v1/ruleenforcements/views?rule_ref=wolfpack.golden_rule') + self.assertEqual(resp.status_int, http_client.OK) + self.assertEqual(len(resp.json), 1) + self.assertEqual(resp.json[0]['rule']['ref'], 'wolfpack.golden_rule') + + def test_get_one_success(self): + resp = self.app.get('/v1/ruleenforcements/views/%s' % (str(self.ENFORCEMENT_1.id))) + self.assertEqual(resp.json['id'], str(self.ENFORCEMENT_1.id)) + + self.assertEqual(resp.json['trigger_instance']['id'], '565e15ce32ed350857dfa623') + self.assertEqual(resp.json['trigger_instance']['payload'], {'foo': 'bar', 'name': 'Joe'}) + + self.assertEqual(resp.json['execution']['action']['ref'], 'core.local') + self.assertEqual(resp.json['execution']['parameters'], {'cmd': 'echo bar'}) diff --git a/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml b/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml new file mode 100644 index 0000000000..b75c9559ae --- /dev/null +++ b/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml @@ -0,0 +1,38 @@ +--- +action: + enabled: true + entry_point: '' + id: 54c6bb640640fd5211edef0c + uid: action:core:local + ref: core.local + name: local + pack: core + parameters: + sudo: + immutable: true + runner_type: run-local +end_timestamp: '2014-09-01T00:00:05.000000Z' +id: 565e15ce32ed350857dfa626 +liveaction: + action: core.someworkflow + callback: {} + context: + user: system + end_timestamp: '2014-09-01T00:00:05.000000Z' + id: 54c6b6d60640fd4f5354e74a + parameters: {} + result: {} + start_timestamp: '2014-09-01T00:00:01.000000Z' + status: scheduled +parameters: + cmd: echo bar +result: {} +runner: + description: A runner for launching linear action chains. + enabled: true + id: 54c6bb640640fd5211edef0b + name: action-chain + runner_module: action_chain_runner + runner_parameters: {} +start_timestamp: '2014-09-01T00:00:01.000000Z' +status: scheduled diff --git a/st2tests/st2tests/fixtures/rule_enforcements/triggerinstances/trigger_instance_1.yaml b/st2tests/st2tests/fixtures/rule_enforcements/triggerinstances/trigger_instance_1.yaml new file mode 100644 index 0000000000..9b81e8312e --- /dev/null +++ b/st2tests/st2tests/fixtures/rule_enforcements/triggerinstances/trigger_instance_1.yaml @@ -0,0 +1,8 @@ +--- +id: 565e15ce32ed350857dfa623 +occurrence_time: '2014-09-01T00:00:01.000000Z' +payload: + foo: bar + name: Joe +trigger: dummy_pack_1.46f67652-20cd-4bab-94e2-4615baa846d0 +status: processed From e0405265c2464b52ebf94a709df693b0b1cdf16e Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 11:22:41 +0200 Subject: [PATCH 28/40] Also include action.parameters and runner.runner_parameters attributes in the execution object of rules view API endpoint result. --- .../controllers/v1/rule_enforcement_views.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/st2api/st2api/controllers/v1/rule_enforcement_views.py b/st2api/st2api/controllers/v1/rule_enforcement_views.py index 896c7435fc..544b043ad8 100644 --- a/st2api/st2api/controllers/v1/rule_enforcement_views.py +++ b/st2api/st2api/controllers/v1/rule_enforcement_views.py @@ -15,6 +15,7 @@ from st2common.models.api.rule_enforcement import RuleEnforcementViewAPI from st2common.models.api.trigger import TriggerInstanceAPI +from st2common.models.api.execution import ActionExecutionAPI from st2common.persistence.rule_enforcement import RuleEnforcement from st2common.persistence.execution import ActionExecution from st2common.persistence.trigger import TriggerInstance @@ -83,8 +84,19 @@ def _append_view_properties(self, rule_enforcement_apis): # 1. Retrieve corresponding execution objects # NOTE: Executions contain a lot of field and could contain a lot of data so we only # retrieve fields we need + only_fields = [ + 'id', + + 'action.ref', + 'action.parameters', + + 'runner.name', + 'runner.runner_parameters', + + 'parameters' + ] execution_dbs = ActionExecution.query(id__in=execution_ids, - only_fields=['id', 'action.ref', 'parameters']) + only_fields=only_fields) execution_dbs_by_id = {} for execution_db in execution_dbs: @@ -114,12 +126,8 @@ def _append_view_properties(self, rule_enforcement_apis): rule_enforcement_api['trigger_instance'] = trigger_instance_api if execution_db: - rule_enforcement_api['execution'] = { - 'action': { - 'ref': execution_db['action']['ref'] - }, - 'parameters': execution_db['parameters'] - } + execution_api = ActionExecutionAPI.from_model(execution_db) + rule_enforcement_api['execution'] = execution_api return rule_enforcement_apis From 61c1b4ff2ee45a37b106b15bb1b18ae1b9372c9c Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 11:27:31 +0200 Subject: [PATCH 29/40] Update affected tests. --- .../unit/controllers/v1/test_rule_enforcement_views.py | 10 ++++++++++ .../rule_enforcements/executions/execution1.yaml | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py index eccb528974..ca3e03151e 100644 --- a/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py +++ b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py @@ -55,6 +55,11 @@ def test_get_all(self): self.assertEqual(resp.json[0]['trigger_instance']['payload'], {'foo': 'bar', 'name': 'Joe'}) self.assertEqual(resp.json[0]['execution']['action']['ref'], 'core.local') + self.assertEqual(resp.json[0]['execution']['action']['parameters'], + {'sudo': {'immutable': True}}) + self.assertEqual(resp.json[0]['execution']['runner']['name'], 'action-chain') + self.assertEqual(resp.json[0]['execution']['runner']['runner_parameters'], + {'foo': {'type': 'string'}}) self.assertEqual(resp.json[0]['execution']['parameters'], {'cmd': 'echo bar'}) self.assertEqual(resp.json[1]['trigger_instance'], {}) @@ -77,4 +82,9 @@ def test_get_one_success(self): self.assertEqual(resp.json['trigger_instance']['payload'], {'foo': 'bar', 'name': 'Joe'}) self.assertEqual(resp.json['execution']['action']['ref'], 'core.local') + self.assertEqual(resp.json['execution']['action']['parameters'], + {'sudo': {'immutable': True}}) + self.assertEqual(resp.json['execution']['runner']['name'], 'action-chain') + self.assertEqual(resp.json['execution']['runner']['runner_parameters'], + {'foo': {'type': 'string'}}) self.assertEqual(resp.json['execution']['parameters'], {'cmd': 'echo bar'}) diff --git a/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml b/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml index b75c9559ae..b76bd99d57 100644 --- a/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml +++ b/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml @@ -33,6 +33,8 @@ runner: id: 54c6bb640640fd5211edef0b name: action-chain runner_module: action_chain_runner - runner_parameters: {} + runner_parameters: + foo: + type: "string" start_timestamp: '2014-09-01T00:00:01.000000Z' status: scheduled From 2ab0358ae18e9750600468227f5445ed9ab00d7e Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 11:29:49 +0200 Subject: [PATCH 30/40] We also need execution.status attribute. --- st2api/st2api/controllers/v1/rule_enforcement_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/st2api/st2api/controllers/v1/rule_enforcement_views.py b/st2api/st2api/controllers/v1/rule_enforcement_views.py index 544b043ad8..5e171f7cbe 100644 --- a/st2api/st2api/controllers/v1/rule_enforcement_views.py +++ b/st2api/st2api/controllers/v1/rule_enforcement_views.py @@ -93,7 +93,8 @@ def _append_view_properties(self, rule_enforcement_apis): 'runner.name', 'runner.runner_parameters', - 'parameters' + 'parameters', + 'status' ] execution_dbs = ActionExecution.query(id__in=execution_ids, only_fields=only_fields) From f2f9eea04e801bd2e6851c05cc759ab20609a938 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 12:47:36 +0200 Subject: [PATCH 31/40] Update affected tests. --- st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py index ca3e03151e..2e8a29002d 100644 --- a/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py +++ b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py @@ -61,6 +61,7 @@ def test_get_all(self): self.assertEqual(resp.json[0]['execution']['runner']['runner_parameters'], {'foo': {'type': 'string'}}) self.assertEqual(resp.json[0]['execution']['parameters'], {'cmd': 'echo bar'}) + self.assertEqual(resp.json[0]['execution']['status'], 'scheduled') self.assertEqual(resp.json[1]['trigger_instance'], {}) self.assertEqual(resp.json[1]['execution'], {}) @@ -88,3 +89,4 @@ def test_get_one_success(self): self.assertEqual(resp.json['execution']['runner']['runner_parameters'], {'foo': {'type': 'string'}}) self.assertEqual(resp.json['execution']['parameters'], {'cmd': 'echo bar'}) + self.assertEqual(resp.json['execution']['status'], 'scheduled') From 546555daf851a42538e54901c0f606909fe9377a Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 13:48:40 +0200 Subject: [PATCH 32/40] Try build workaround. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99ae2feda0..161cb3622c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ cache: pip: true directories: - virtualenv/ - - .tox/ + #- .tox/ before_install: # Work around for Travis timeout issues, see https://github.com/travis-ci/travis-ci/issues/9112 From 68c3267e27554cebadc853d18f1ab33b6ea12ccc Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 13:59:30 +0200 Subject: [PATCH 33/40] Fix openapi ordering - in Python 3 ordering matters and if the entries are not ordered correctly, router will match a wrong path. --- st2common/st2common/openapi.yaml | 56 ++++++++++++++--------------- st2common/st2common/openapi.yaml.j2 | 56 ++++++++++++++--------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index 5679723995..01235e7aa4 100644 --- a/st2common/st2common/openapi.yaml +++ b/st2common/st2common/openapi.yaml @@ -3314,34 +3314,6 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' - /api/v1/ruleenforcements/{id}: - get: - operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one - description: Return a specific rule enforcement based on id. - parameters: - - name: id - in: path - description: Entity id - type: string - required: true - x-parameters: - - name: user - in: context - x-as: requester_user - description: User performing the operation. - responses: - '200': - description: Rule Enforcements based on ref or id - schema: - $ref: '#/definitions/RuleEnforcementsList' - examples: - application/json: - ref: 'core.local' - # and stuff - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' /api/v1/ruleenforcements/views: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all @@ -3422,6 +3394,34 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + /api/v1/ruleenforcements/{id}: + get: + operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one + description: Return a specific rule enforcement based on id. + parameters: + - name: id + in: path + description: Entity id + type: string + required: true + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: Rule Enforcements based on ref or id + schema: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' /api/v1/ruleenforcements/views/{id}: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one diff --git a/st2common/st2common/openapi.yaml.j2 b/st2common/st2common/openapi.yaml.j2 index 7a4ba0742c..b3ff5d631a 100644 --- a/st2common/st2common/openapi.yaml.j2 +++ b/st2common/st2common/openapi.yaml.j2 @@ -3310,34 +3310,6 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' - /api/v1/ruleenforcements/{id}: - get: - operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one - description: Return a specific rule enforcement based on id. - parameters: - - name: id - in: path - description: Entity id - type: string - required: true - x-parameters: - - name: user - in: context - x-as: requester_user - description: User performing the operation. - responses: - '200': - description: Rule Enforcements based on ref or id - schema: - $ref: '#/definitions/RuleEnforcementsList' - examples: - application/json: - ref: 'core.local' - # and stuff - default: - description: Unexpected error - schema: - $ref: '#/definitions/Error' /api/v1/ruleenforcements/views: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_all @@ -3418,6 +3390,34 @@ paths: description: Unexpected error schema: $ref: '#/definitions/Error' + /api/v1/ruleenforcements/{id}: + get: + operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one + description: Return a specific rule enforcement based on id. + parameters: + - name: id + in: path + description: Entity id + type: string + required: true + x-parameters: + - name: user + in: context + x-as: requester_user + description: User performing the operation. + responses: + '200': + description: Rule Enforcements based on ref or id + schema: + $ref: '#/definitions/RuleEnforcementsList' + examples: + application/json: + ref: 'core.local' + # and stuff + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' /api/v1/ruleenforcements/views/{id}: get: operationId: st2api.controllers.v1.rule_enforcement_views:rule_enforcement_view_controller.get_one From 278ad62be61de490ba7fe87f396a69f600e26ce6 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 14:05:20 +0200 Subject: [PATCH 34/40] Make tests more robust. --- st2client/tests/unit/test_formatters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/st2client/tests/unit/test_formatters.py b/st2client/tests/unit/test_formatters.py index 19bb8b1316..bfe8c0b8e4 100644 --- a/st2client/tests/unit/test_formatters.py +++ b/st2client/tests/unit/test_formatters.py @@ -149,6 +149,7 @@ def test_execution_unicode(self): if six.PY2: self.assertEqual(content, FIXTURES['results']['execution_unicode.txt']) else: + content = content.replace(r'\xE2\x80\xA1', r'\u2021') self.assertEqual(content, FIXTURES['results']['execution_unicode_py3.txt']) def test_execution_get_detail_in_json(self): From faf150d916fbdbe8b53696e5a472781e47a3fae5 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 14:07:18 +0200 Subject: [PATCH 35/40] Add a comment. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 161cb3622c..6dad816791 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,8 @@ cache: pip: true directories: - virtualenv/ + # NOTE: Caching .tox speeds up py3 build for 30-60 seconds, but causes issues when dependencies + # are updated so it's disabled #- .tox/ before_install: From 2ff07886862d682d5c4966b3912a249547394a9e Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 14:47:53 +0200 Subject: [PATCH 36/40] Try a change. --- .travis.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6dad816791..8aca1bfcd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ cache: - virtualenv/ # NOTE: Caching .tox speeds up py3 build for 30-60 seconds, but causes issues when dependencies # are updated so it's disabled - #- .tox/ + - .tox/ before_install: # Work around for Travis timeout issues, see https://github.com/travis-ci/travis-ci/issues/9112 diff --git a/Makefile b/Makefile index eb5e4bb2dc..614fb4be00 100644 --- a/Makefile +++ b/Makefile @@ -510,7 +510,7 @@ ci-py3-unit: @echo @echo "==================== ci-py3-unit ====================" @echo - tox -e py36 -vv + tox --recreate -e py36 -vv .PHONY: .rst-check .rst-check: From 914de0ab9c57565f96ef89cd26237c5d0b25889c Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Wed, 23 May 2018 15:34:54 +0200 Subject: [PATCH 37/40] Revert "Try a change." This reverts commit 2ff07886862d682d5c4966b3912a249547394a9e. --- .travis.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8aca1bfcd2..6dad816791 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ cache: - virtualenv/ # NOTE: Caching .tox speeds up py3 build for 30-60 seconds, but causes issues when dependencies # are updated so it's disabled - - .tox/ + #- .tox/ before_install: # Work around for Travis timeout issues, see https://github.com/travis-ci/travis-ci/issues/9112 diff --git a/Makefile b/Makefile index 614fb4be00..eb5e4bb2dc 100644 --- a/Makefile +++ b/Makefile @@ -510,7 +510,7 @@ ci-py3-unit: @echo @echo "==================== ci-py3-unit ====================" @echo - tox --recreate -e py36 -vv + tox -e py36 -vv .PHONY: .rst-check .rst-check: From 71ee8870b71b8072ecf5961cfadfd52c59bff999 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Thu, 24 May 2018 11:22:08 +0200 Subject: [PATCH 38/40] Add new "status" field to the RuleEnforcementDB object. For backward compatibility reasons, default it to "succeeded". --- .../st2common/constants/rule_enforcement.py | 29 +++++++++++++++++++ .../st2common/models/api/rule_enforcement.py | 13 +++++++-- st2common/st2common/models/db/execution.py | 1 + .../st2common/models/db/rule_enforcement.py | 12 ++++++++ st2reactor/st2reactor/rules/enforcer.py | 4 +++ st2reactor/st2reactor/rules/filter.py | 4 ++- 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 st2common/st2common/constants/rule_enforcement.py diff --git a/st2common/st2common/constants/rule_enforcement.py b/st2common/st2common/constants/rule_enforcement.py new file mode 100644 index 0000000000..13be4a4dc4 --- /dev/null +++ b/st2common/st2common/constants/rule_enforcement.py @@ -0,0 +1,29 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = [ + 'RULE_ENFORCEMENT_STATUS_SUCCEEDED', + 'RULE_ENFORCEMENT_STATUS_FAILED', + + 'RULE_ENFORCEMENT_STATUSES' +] + +RULE_ENFORCEMENT_STATUS_SUCCEEDED = 'succeeded' +RULE_ENFORCEMENT_STATUS_FAILED = 'failed' + +RULE_ENFORCEMENT_STATUSES = [ + RULE_ENFORCEMENT_STATUS_SUCCEEDED, + RULE_ENFORCEMENT_STATUS_FAILED +] diff --git a/st2common/st2common/models/api/rule_enforcement.py b/st2common/st2common/models/api/rule_enforcement.py index cc95b05514..b02b849db2 100644 --- a/st2common/st2common/models/api/rule_enforcement.py +++ b/st2common/st2common/models/api/rule_enforcement.py @@ -24,6 +24,8 @@ from st2common.models.db.rule_enforcement import RuleReferenceSpecDB from st2common.models.api.execution import ActionExecutionAPI from st2common.models.api.trigger import TriggerInstanceAPI +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_SUCCEEDED +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUSES from st2common.util import isotime __all__ = [ @@ -81,7 +83,12 @@ class RuleEnforcementAPI(BaseAPI): 'description': 'Timestamp when rule enforcement happened.', 'type': 'string', 'required': True - } + }, + "status": { + "description": "Rule enforcement status.", + "type": "string", + "enum": RULE_ENFORCEMENT_STATUSES + }, }, 'additionalProperties': False } @@ -92,6 +99,7 @@ def to_model(cls, rule_enforcement): execution_id = getattr(rule_enforcement, 'execution_id', None) enforced_at = getattr(rule_enforcement, 'enforced_at', None) failure_reason = getattr(rule_enforcement, 'failure_reason', None) + status = getattr(rule_enforcement, 'status', RULE_ENFORCEMENT_STATUS_SUCCEEDED) rule_ref_model = dict(getattr(rule_enforcement, 'rule', {})) rule = RuleReferenceSpecDB(ref=rule_ref_model['ref'], id=rule_ref_model['id'], @@ -101,7 +109,8 @@ def to_model(cls, rule_enforcement): enforced_at = isotime.parse(enforced_at) return cls.model(trigger_instance_id=trigger_instance_id, execution_id=execution_id, - failure_reason=failure_reason, enforced_at=enforced_at, rule=rule) + failure_reason=failure_reason, enforced_at=enforced_at, rule=rule, + status=status) @classmethod def from_model(cls, model, mask_secrets=False): diff --git a/st2common/st2common/models/db/execution.py b/st2common/st2common/models/db/execution.py index 1012f2298b..f8e55b88db 100644 --- a/st2common/st2common/models/db/execution.py +++ b/st2common/st2common/models/db/execution.py @@ -26,6 +26,7 @@ from st2common.util.secrets import mask_inquiry_response from st2common.util.secrets import mask_secret_parameters from st2common.constants.types import ResourceType + __all__ = [ 'ActionExecutionDB', 'ActionExecutionOutputDB' diff --git a/st2common/st2common/models/db/rule_enforcement.py b/st2common/st2common/models/db/rule_enforcement.py index b90a7fd942..cf801b5fc0 100644 --- a/st2common/st2common/models/db/rule_enforcement.py +++ b/st2common/st2common/models/db/rule_enforcement.py @@ -14,12 +14,19 @@ # limitations under the License. from __future__ import absolute_import + import mongoengine as me from st2common.fields import ComplexDateTimeField from st2common.models.db import MongoDBAccess from st2common.models.db import stormbase from st2common.util import date as date_utils +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_SUCCEEDED + +__all__ = [ + 'RuleReferenceSpecDB', + 'RuleEnforcementDB' +] class RuleReferenceSpecDB(me.EmbeddedDocument): @@ -52,6 +59,10 @@ class RuleEnforcementDB(stormbase.StormFoundationDB, stormbase.TagsMixin): enforced_at = ComplexDateTimeField( default=date_utils.get_datetime_utc_now, help_text='The timestamp when the rule enforcement happened.') + status = me.StringField( + required=True, + default=RULE_ENFORCEMENT_STATUS_SUCCEEDED, + help_text='Rule enforcement status.') meta = { 'indexes': [ @@ -62,6 +73,7 @@ class RuleEnforcementDB(stormbase.StormFoundationDB, stormbase.TagsMixin): {'fields': ['enforced_at']}, {'fields': ['-enforced_at']}, {'fields': ['-enforced_at', 'rule.ref']}, + {'fields': ['status']}, ] + stormbase.TagsMixin.get_indices() } diff --git a/st2reactor/st2reactor/rules/enforcer.py b/st2reactor/st2reactor/rules/enforcer.py index b629527363..2bf71c358e 100644 --- a/st2reactor/st2reactor/rules/enforcer.py +++ b/st2reactor/st2reactor/rules/enforcer.py @@ -23,6 +23,8 @@ from st2common.constants import action as action_constants from st2common.constants.trace import TRACE_CONTEXT from st2common.constants.rules import TRIGGER_PAYLOAD_PREFIX +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_SUCCEEDED +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED from st2common.models.api.trace import TraceContext from st2common.models.db.liveaction import LiveActionDB from st2common.models.db.rule_enforcement import RuleEnforcementDB @@ -94,9 +96,11 @@ def enforce(self): execution_db = self._do_enforce() # pylint: disable=no-member enforcement_db.execution_id = str(execution_db.id) + enforcement_db.status = RULE_ENFORCEMENT_STATUS_SUCCEEDED extra['execution_db'] = execution_db except Exception as e: # Record the failure reason in the RuleEnforcement. + enforcement_db.status = RULE_ENFORCEMENT_STATUS_FAILED enforcement_db.failure_reason = str(e) LOG.exception('Failed kicking off execution for rule %s.', self.rule, extra=extra) finally: diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py index 83ce913214..87a5ec6b3d 100644 --- a/st2reactor/st2reactor/rules/filter.py +++ b/st2reactor/st2reactor/rules/filter.py @@ -24,6 +24,7 @@ from st2common import operators as criteria_operators from st2common.constants.rules import RULE_TYPE_BACKSTOP from st2common.constants.rules import MATCH_CRITERIA +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED from st2common.models.db.rule_enforcement import RuleEnforcementDB from st2common.persistence.rule_enforcement import RuleEnforcement @@ -235,7 +236,8 @@ def _create_rule_enforcement(self, failure_reason, exc): 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, - failure_reason=failure_reason) + failure_reason=failure_reason, + status=RULE_ENFORCEMENT_STATUS_FAILED) try: RuleEnforcement.add_or_update(enforcement_db) From cc08bbc24a0ba8b99fb0935034d513e642597474 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Thu, 24 May 2018 11:38:33 +0200 Subject: [PATCH 39/40] Update affected tests. --- st2reactor/tests/unit/test_enforce.py | 18 ++++++++++++++++++ st2reactor/tests/unit/test_rule_matcher.py | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/st2reactor/tests/unit/test_enforce.py b/st2reactor/tests/unit/test_enforce.py index 84651855c2..72779b5e40 100644 --- a/st2reactor/tests/unit/test_enforce.py +++ b/st2reactor/tests/unit/test_enforce.py @@ -19,6 +19,8 @@ from st2common.constants import action as action_constants from st2common.constants.keyvalue import FULL_SYSTEM_SCOPE +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_SUCCEEDED +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED from st2common.models.db.trigger import TriggerInstanceDB from st2common.models.db.execution import ActionExecutionDB from st2common.models.db.liveaction import LiveActionDB @@ -146,6 +148,8 @@ def test_ruleenforcement_create_on_success(self): self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, self.models['rules']['rule2.yaml'].ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) @mock.patch.object(action_service, 'request', mock.MagicMock( return_value=(MOCK_LIVEACTION, MOCK_EXECUTION))) @@ -173,6 +177,8 @@ def mock_cast_string(x): self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, self.models['rules']['rule_use_none_filter.yaml'].ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) # 2. Verify that None type from trigger instance is correctly serialized to # None when using "use_none" Jinja filter when invoking an action @@ -195,6 +201,8 @@ def mock_cast_string(x): self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, self.models['rules']['rule_use_none_filter.yaml'].ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) casts.CASTS['string'] = casts._cast_string @@ -215,6 +223,8 @@ def mock_cast_string(x): self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, self.models['rules']['rule_none_no_use_none_filter.yaml'].ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) casts.CASTS['string'] = casts._cast_string @@ -228,6 +238,8 @@ def test_ruleenforcement_create_on_fail(self): self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].failure_reason, FAILURE_REASON) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_FAILED) @mock.patch.object(action_service, 'request', mock.MagicMock( return_value=(MOCK_LIVEACTION, MOCK_EXECUTION))) @@ -244,6 +256,8 @@ def test_action_default_jinja_parameter_value_is_rendered(self): self.assertTrue(execution_db is not None) self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, rule.ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) call_parameters = action_service.request.call_args[0][0].parameters @@ -264,6 +278,8 @@ def test_action_default_jinja_parameter_value_overridden_in_rule(self): self.assertTrue(execution_db is not None) self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, rule.ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_SUCCEEDED) call_parameters = action_service.request.call_args[0][0].parameters @@ -288,6 +304,8 @@ def test_action_default_jinja_parameter_value_render_fail(self): self.assertTrue(execution_db is None) self.assertTrue(RuleEnforcement.add_or_update.called) self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].rule.ref, rule.ref) + self.assertEqual(RuleEnforcement.add_or_update.call_args[0][0].status, + RULE_ENFORCEMENT_STATUS_FAILED) self.assertFalse(action_service.request.called) self.assertTrue(action_service.create_request.called) diff --git a/st2reactor/tests/unit/test_rule_matcher.py b/st2reactor/tests/unit/test_rule_matcher.py index a42b8ff7bb..3efeeeccf0 100644 --- a/st2reactor/tests/unit/test_rule_matcher.py +++ b/st2reactor/tests/unit/test_rule_matcher.py @@ -27,6 +27,7 @@ import st2reactor.container.utils as container_utils from st2reactor.rules.matcher import RulesMatcher from st2common.persistence.rule_enforcement import RuleEnforcement +from st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED from st2tests.base import DbTestCase from st2tests.base import CleanDbTestCase @@ -209,6 +210,7 @@ def test_rule_enforcement_is_created_on_exception_1(self): self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + self.assertEqual(rule_enforcement_dbs[0].status, RULE_ENFORCEMENT_STATUS_FAILED) @mock.patch('st2reactor.rules.filter.PayloadLookup.get_value', mock.Mock(side_effect=Exception('exception in get_value'))) @@ -241,6 +243,7 @@ def test_rule_enforcement_is_created_on_exception_2(self): self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + self.assertEqual(rule_enforcement_dbs[0].status, RULE_ENFORCEMENT_STATUS_FAILED) @mock.patch('st2common.operators.get_operator', mock.Mock(return_value=mock.Mock(side_effect=Exception('exception in equals')))) @@ -274,6 +277,7 @@ def test_rule_enforcement_is_created_on_exception_3(self): self.assertEqual(rule_enforcement_dbs[0].failure_reason, expected_failure) self.assertEqual(rule_enforcement_dbs[0].trigger_instance_id, str(trigger_instance.id)) self.assertEqual(rule_enforcement_dbs[0].rule['id'], str(rule_4_db.id)) + self.assertEqual(rule_enforcement_dbs[0].status, RULE_ENFORCEMENT_STATUS_FAILED) def _setup_sample_trigger(self, name): trigtype = TriggerTypeDB(name=name, pack='dummy_pack_1', payload_schema={}, From f9b2753e135f6180d8abe470fa0f63b734925ff5 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Thu, 24 May 2018 11:46:03 +0200 Subject: [PATCH 40/40] Add changelog entry. --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4030563aa9..022e762a9c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,10 @@ Added * Add new ``GET /v1/ruleenforcements/views[/]`` API endpoints which allow user to retrieve RuleEnforcement objects with the corresponding TriggerInstance and Execution objects. (new feature) #4134 +* Add new ``status`` field to the ``RuleEnforcement`` model. This field can contain the following + values - ``succeeded`` (trigger instance matched a rule and action execution was triggered + successfully), ``failed`` (trigger instance matched a rule, but it didn't result in an action + execution due to Jinja rendering failure or other exception). (improvement) #4134 Changed ~~~~~~~