diff --git a/.travis.yml b/.travis.yml index 5592cc27a5..6dad816791 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,9 @@ cache: pip: true directories: - virtualenv/ - - .tox/ + # 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: # Work around for Travis timeout issues, see https://github.com/travis-ci/travis-ci/issues/9112 @@ -53,7 +55,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 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da5fff73d4..022e762a9c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,19 @@ 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 +* 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 ~~~~~~~ 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..c8c54c9f22 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 @@ -16,7 +17,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,15 +27,18 @@ 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 pymongo==3.6.1 +pyrabbit python-dateutil 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 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/rule_enforcement_views.py b/st2api/st2api/controllers/v1/rule_enforcement_views.py new file mode 100644 index 0000000000..5e171f7cbe --- /dev/null +++ b/st2api/st2api/controllers/v1/rule_enforcement_views.py @@ -0,0 +1,136 @@ +# 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.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 +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 we include those fields: + + * trigger_instance object for each rule enforcement object + * subset of an execution object in case execution was triggered + """ + + 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): + """ + 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 + only_fields = [ + 'id', + + 'action.ref', + 'action.parameters', + + 'runner.name', + 'runner.runner_parameters', + + 'parameters', + 'status' + ] + execution_dbs = ActionExecution.query(id__in=execution_ids, + only_fields=only_fields) + + 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'] = {} + + 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 trigger_instance_db: + trigger_instance_api = TriggerInstanceAPI.from_model(trigger_instance_db) + rule_enforcement_api['trigger_instance'] = trigger_instance_api + + if execution_db: + execution_api = ActionExecutionAPI.from_model(execution_db) + rule_enforcement_api['execution'] = execution_api + + return rule_enforcement_apis + + +rule_enforcement_view_controller = RuleEnforcementViewController() diff --git a/st2api/st2api/controllers/v1/rule_enforcements.py b/st2api/st2api/controllers/v1/rule_enforcements.py index 687a3b9eaa..47fc6be1f0 100644 --- a/st2api/st2api/controllers/v1/rule_enforcements.py +++ b/st2api/st2api/controllers/v1/rule_enforcements.py @@ -21,7 +21,15 @@ 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', + 'QUERY_OPTIONS', + 'FILTER_TRANSFORM_FUNCTIONS' +] http_client = six.moves.http_client @@ -39,23 +47,27 @@ '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(resource.ResourceController): +class RuleEnforcementController(ResourceController): model = RuleEnforcementAPI 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, 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/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..2e8a29002d --- /dev/null +++ b/st2api/tests/unit/controllers/v1/test_rule_enforcement_views.py @@ -0,0 +1,92 @@ +# 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']['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[0]['execution']['status'], 'scheduled') + + 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']['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'}) + self.assertEqual(resp.json['execution']['status'], 'scheduled') 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): 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 166b2448c5..b02b849db2 100644 --- a/st2common/st2common/models/api/rule_enforcement.py +++ b/st2common/st2common/models/api/rule_enforcement.py @@ -14,12 +14,27 @@ # 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.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__ = [ + 'RuleEnforcementAPI', + 'RuleEnforcementViewAPI', + + 'RuleReferenceSpecDB' +] + class RuleReferenceSpec(BaseAPI): schema = { @@ -68,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 } @@ -79,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'], @@ -88,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): @@ -97,3 +119,14 @@ 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) + + # Update the schema to include additional trigger instance properties + schema['properties']['trigger_instance'] = copy.deepcopy(TriggerInstanceAPI.schema) 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/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) diff --git a/st2common/st2common/openapi.yaml b/st2common/st2common/openapi.yaml index d949403b62..01235e7aa4 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: @@ -3314,6 +3314,86 @@ 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/{id}: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one @@ -3342,6 +3422,35 @@ paths: 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..b3ff5d631a 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: @@ -3310,6 +3310,86 @@ 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/{id}: get: operationId: st2api.controllers.v1.rule_enforcements:rule_enforcements_controller.get_one @@ -3338,6 +3418,35 @@ paths: 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/st2reactor/st2reactor/rules/enforcer.py b/st2reactor/st2reactor/rules/enforcer.py index 13b814aabd..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,10 +96,12 @@ 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.failure_reason = e.message + 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: self._update_enforcement(enforcement_db) diff --git a/st2reactor/st2reactor/rules/filter.py b/st2reactor/st2reactor/rules/filter.py index 5ad73a2c2f..87a5ec6b3d 100644 --- a/st2reactor/st2reactor/rules/filter.py +++ b/st2reactor/st2reactor/rules/filter.py @@ -14,17 +14,27 @@ # 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.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED +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') @@ -118,9 +128,12 @@ def _check_criterion(self, criterion_k, criterion_v, payload_lookup): criteria_pattern=criteria_pattern, 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) + 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, exc=e) + return (False, None, None) try: @@ -130,9 +143,11 @@ 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: - LOG.exception('Failed transforming criteria key %s', criterion_k, - extra=self._base_logger_context) + 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, exc=e) + return (False, None, None) op_func = criteria_operators.get_operator(criteria_operator) @@ -144,9 +159,11 @@ 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: - LOG.exception('There might be a problem with the criteria in rule %s.', self.rule, - extra=self._base_logger_context) + 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, exc=e) + return (False, None, None) return result, payload_value, criteria_pattern @@ -207,6 +224,29 @@ def _render_criteria_pattern(self, criteria_pattern, criteria_context): return criteria_rendered + 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: %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, + failure_reason=failure_reason, + status=RULE_ENFORCEMENT_STATUS_FAILED) + + 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): """ diff --git a/st2reactor/tests/integration/test_garbage_collector.py b/st2reactor/tests/integration/test_garbage_collector.py index edb4660eb4..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(5) + eventlet.sleep(10) process.send_signal(signal.SIGKILL) self.remove_process(process=process) 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: 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 70368de309..3efeeeccf0 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,132 @@ 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 st2common.constants.rule_enforcement import RULE_ENFORCEMENT_STATUS_FAILED + 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 +159,126 @@ 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": 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)) + 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'))) + 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: ' + '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)) + 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')))) + 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: 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)) + 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={}, parameters_schema={}) @@ -70,96 +288,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 +305,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. diff --git a/st2tests/in-requirements.txt b/st2tests/in-requirements.txt index 46023e4f09..ab9c28e1f0 100644 --- a/st2tests/in-requirements.txt +++ b/st2tests/in-requirements.txt @@ -6,3 +6,5 @@ psutil webtest nose-timer rednose +RandomWords +pyrabbit 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..b76bd99d57 --- /dev/null +++ b/st2tests/st2tests/fixtures/rule_enforcements/executions/execution1.yaml @@ -0,0 +1,40 @@ +--- +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: + foo: + type: "string" +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 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 diff --git a/tox.ini b/tox.ini index 6301c4d144..6a3def8e9e 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,9 @@ deps = virtualenv -e{toxinidir}/st2client -e{toxinidir}/st2common commands = - nosetests --with-timer --rednose -sv st2client/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/ --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/