diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9c7da4d7de..ed0d048534 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,11 @@ in development * Allow user to pass a boolean value for the ``cacert`` st2client constructor argument. This way it now mimics the behavior of the ``verify`` argument of the ``requests.request`` method. (improvement) -* Add datastore access to Python actions. (new-feature) #2396 [Kale Blankenship] +* Add datastore access to Python runner actions via the ``action_service`` which is available + to all the Python runner actions after instantiation. (new-feature) #2396 #2511 + [Kale Blankenship] +* Update ``st2actions.runners.pythonrunner.Action`` class so the constructor also takes + ``action_service`` as the second argument. * Allow /v1/webhooks API endpoint request body to either be JSON or url encoded form data. Request body type is determined and parsed accordingly based on the value of ``Content-Type`` header. diff --git a/Makefile b/Makefile index f383400476..bbe3774ce8 100644 --- a/Makefile +++ b/Makefile @@ -68,9 +68,10 @@ pylint: requirements .pylint . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models $$component/$$component || exit 1; \ done # Lint Python pack management actions - . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models contrib/packs/actions/pack_mgmt/ || exit 1; + . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models contrib/packs/actions/ || exit 1; # Lint other packs . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models contrib/linux || exit 1; + . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models contrib/chatops || exit 1; # Lint Python scripts . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models scripts/*.py || exit 1; . $(VIRTUALENV_DIR)/bin/activate; pylint -E --rcfile=./lint-configs/python/.pylintrc --load-plugins=pylint_plugins.api_models tools/*.py || exit 1; @@ -84,8 +85,9 @@ flake8: requirements .flake8 @echo "==================== flake ====================" @echo . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 $(COMPONENTS) - . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 contrib/packs/actions/pack_mgmt/ + . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 contrib/packs/actions/ . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 contrib/linux + . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 contrib/chatops/ . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 scripts/ . $(VIRTUALENV_DIR)/bin/activate; flake8 --config ./lint-configs/python/.flake8 tools/ diff --git a/contrib/chatops/actions/format_execution_result.py b/contrib/chatops/actions/format_execution_result.py index 55836bee23..8873d36b6a 100755 --- a/contrib/chatops/actions/format_execution_result.py +++ b/contrib/chatops/actions/format_execution_result.py @@ -7,8 +7,8 @@ class FormatResultAction(Action): - def __init__(self, config): - super(FormatResultAction, self).__init__(config) + def __init__(self, config=None, action_service=None): + super(FormatResultAction, self).__init__(config=config, action_service=action_service) api_url = os.environ.get('ST2_ACTION_API_URL', None) token = os.environ.get('ST2_ACTION_AUTH_TOKEN', None) self.client = Client(api_url=api_url, token=token) @@ -51,4 +51,4 @@ def _get_execution(self, execution_id): if not execution: return None excludes = ["trigger", "trigger_type", "trigger_instance", "liveaction"] - return execution.to_dict(exclude_attributes= excludes) + return execution.to_dict(exclude_attributes=excludes) diff --git a/contrib/examples/actions/pythonactions/__init__.py b/contrib/examples/actions/pythonactions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/examples/actions/pythonactions/isprime.py b/contrib/examples/actions/pythonactions/isprime.py index f38ff8abef..e57bf79456 100644 --- a/contrib/examples/actions/pythonactions/isprime.py +++ b/contrib/examples/actions/pythonactions/isprime.py @@ -1,12 +1,11 @@ import math +from st2actions.runners.pythonrunner import Action -class PrimeChecker(object): - - def __init__(self, config=None): - pass +class PrimeCheckerAction(Action): def run(self, value=0): + self.logger.debug('value=%s' % (value)) if math.floor(value) != value: raise ValueError('%s should be an integer.' % value) if value < 2: @@ -17,6 +16,6 @@ def run(self, value=0): return True if __name__ == '__main__': - checker = PrimeChecker() + checker = PrimeCheckerAction() for i in range(0, 10): - print '%s : %s' % (i, checker.run(**{'value': i})) + print '%s : %s' % (i, checker.run(value=1)) diff --git a/contrib/examples/tests/test_action_isprime.py b/contrib/examples/tests/test_action_isprime.py new file mode 100644 index 0000000000..533de72539 --- /dev/null +++ b/contrib/examples/tests/test_action_isprime.py @@ -0,0 +1,15 @@ +from st2tests.base import BaseActionTestCase + +from pythonactions.isprime import PrimeCheckerAction + + +class PrimeCheckerActionTestCase(BaseActionTestCase): + action_cls = PrimeCheckerAction + + def test_run(self): + action = self.get_action_instance() + result = action.run(value=1) + self.assertFalse(result) + + result = action.run(value=3) + self.assertTrue(result) diff --git a/contrib/packs/actions/check_auto_deploy_repo.py b/contrib/packs/actions/check_auto_deploy_repo.py index 8dfb4a7103..45d2d6cd99 100755 --- a/contrib/packs/actions/check_auto_deploy_repo.py +++ b/contrib/packs/actions/check_auto_deploy_repo.py @@ -15,17 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import requests -import json -import getopt -import argparse -import os -import yaml - -from getpass import getpass from st2actions.runners.pythonrunner import Action + class CheckAutoDeployRepo(Action): def run(self, branch, repo_name): """Returns the required data to complete an auto deployment of a pack in repo_name. @@ -42,12 +34,14 @@ def run(self, branch, repo_name): results = {} try: - results['deployment_branch'] = self.config["repositories"][repo_name]["auto_deployment"]["branch"] - results['notify_channel'] = self.config["repositories"][repo_name]["auto_deployment"]["notify_channel"] + repo_config = self.config["repositories"][repo_name] + results['deployment_branch'] = repo_config["auto_deployment"]["branch"] + results['notify_channel'] = repo_config["auto_deployment"]["notify_channel"] except KeyError: raise ValueError("No repositories or auto_deployment config for '%s'" % repo_name) else: if branch == "refs/heads/%s" % results['deployment_branch']: return results else: - raise ValueError("Branch %s for %s should not be auto deployed" % (branch, repo_name)) + raise ValueError("Branch %s for %s should not be auto deployed" % + (branch, repo_name)) diff --git a/contrib/packs/actions/expand_repo_name.py b/contrib/packs/actions/expand_repo_name.py index 845b80cfa3..5b9ca4b7b4 100755 --- a/contrib/packs/actions/expand_repo_name.py +++ b/contrib/packs/actions/expand_repo_name.py @@ -15,17 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import requests -import json -import getopt -import argparse -import os -import yaml - -from getpass import getpass from st2actions.runners.pythonrunner import Action + class ExpandRepoName(Action): def run(self, repo_name): """Returns the data required to install packs from repo_name. diff --git a/contrib/packs/actions/pack_mgmt/delete.py b/contrib/packs/actions/pack_mgmt/delete.py index caada04d00..eebd9635a2 100644 --- a/contrib/packs/actions/pack_mgmt/delete.py +++ b/contrib/packs/actions/pack_mgmt/delete.py @@ -26,8 +26,8 @@ class UninstallPackAction(Action): - def __init__(self, config=None): - super(UninstallPackAction, self).__init__(config=config) + def __init__(self, config=None, action_service=None): + super(UninstallPackAction, self).__init__(config=config, action_service=action_service) self._base_virtualenvs_path = os.path.join(cfg.CONF.system.base_path, 'virtualenvs/') diff --git a/contrib/packs/actions/pack_mgmt/download.py b/contrib/packs/actions/pack_mgmt/download.py index a8a6f33312..e54a0241c9 100644 --- a/contrib/packs/actions/pack_mgmt/download.py +++ b/contrib/packs/actions/pack_mgmt/download.py @@ -61,8 +61,8 @@ class DownloadGitRepoAction(Action): - def __init__(self, config=None): - super(DownloadGitRepoAction, self).__init__(config=config) + def __init__(self, config=None, action_service=None): + super(DownloadGitRepoAction, self).__init__(config=config, action_service=action_service) self._subtree = None self._repo_url = None diff --git a/contrib/packs/actions/pack_mgmt/setup_virtualenv.py b/contrib/packs/actions/pack_mgmt/setup_virtualenv.py index d813dbf673..74b02e6d4a 100644 --- a/contrib/packs/actions/pack_mgmt/setup_virtualenv.py +++ b/contrib/packs/actions/pack_mgmt/setup_virtualenv.py @@ -43,8 +43,9 @@ class SetupVirtualEnvironmentAction(Action): current dependencies as well as an installation of new dependencies """ - def __init__(self, config=None): - super(SetupVirtualEnvironmentAction, self).__init__(config=config) + def __init__(self, config=None, action_service=None): + super(SetupVirtualEnvironmentAction, self).__init__(config=config, + action_service=action_service) self._base_virtualenvs_path = os.path.join(cfg.CONF.system.base_path, 'virtualenvs/') diff --git a/contrib/packs/actions/pack_mgmt/unload.py b/contrib/packs/actions/pack_mgmt/unload.py index 0263eaaf3e..e36c01bff6 100644 --- a/contrib/packs/actions/pack_mgmt/unload.py +++ b/contrib/packs/actions/pack_mgmt/unload.py @@ -31,8 +31,8 @@ class UnregisterPackAction(BaseAction): - def __init__(self, config=None): - super(UnregisterPackAction, self).__init__(config=config) + def __init__(self, config=None, action_service=None): + super(UnregisterPackAction, self).__init__(config=config, action_service=action_service) self.initialize() def initialize(self): diff --git a/contrib/packs/tests/test_action_check_auto_deploy_repo.py b/contrib/packs/tests/test_action_check_auto_deploy_repo.py index 79a81e15c1..9b1b8752b8 100644 --- a/contrib/packs/tests/test_action_check_auto_deploy_repo.py +++ b/contrib/packs/tests/test_action_check_auto_deploy_repo.py @@ -32,30 +32,32 @@ """ class CheckAutoDeployRepoActionTestCase(BaseActionTestCase): + action_cls = CheckAutoDeployRepo + def test_run_config_blank(self): config = yaml.safe_load(MOCK_CONFIG_BLANK) - action = CheckAutoDeployRepo(config) + action = self.get_action_instance(config=config) self.assertRaises(Exception, action.run, branch="refs/heads/master", repo_name="st2contrib") def test_run_repositories_blank(self): config = yaml.safe_load(MOCK_CONFIG_BLANK_REPOSITORIES) - action = CheckAutoDeployRepo(config) + action = self.get_action_instance(config=config) self.assertRaises(Exception, action.run, branch="refs/heads/master", repo_name="st2contrib") def test_run_st2contrib_no_auto_deloy(self): config = yaml.safe_load(MOCK_CONFIG_FULL) - action = CheckAutoDeployRepo(config) + action = self.get_action_instance(config=config) self.assertRaises(Exception, action.run, branch="refs/heads/dev", repo_name="st2contrib") def test_run_st2contrib_auto_deloy(self): config = yaml.safe_load(MOCK_CONFIG_FULL) - action = CheckAutoDeployRepo(config) + action = self.get_action_instance(config=config) expected = {'deployment_branch': 'master', 'notify_channel': 'community'} @@ -72,7 +74,7 @@ def test_run_st2incubator_no_auto_deloy(self): def test_run_st2incubator_auto_deloy(self): config = yaml.safe_load(MOCK_CONFIG_FULL) - action = CheckAutoDeployRepo(config) + action = self.get_action_instance(config=config) expected = {'deployment_branch': 'master', 'notify_channel': 'community'} diff --git a/contrib/packs/tests/test_action_expand_repo_name.py b/contrib/packs/tests/test_action_expand_repo_name.py index 8e2a670499..c2d4088a79 100644 --- a/contrib/packs/tests/test_action_expand_repo_name.py +++ b/contrib/packs/tests/test_action_expand_repo_name.py @@ -30,23 +30,25 @@ """ class ExpandRepoNameTestCase(BaseActionTestCase): + action_cls = ExpandRepoName + def test_run_config_blank(self): config = yaml.safe_load(MOCK_CONFIG_BLANK) - action = ExpandRepoName(config) + action = self.get_action_instance(config=config) self.assertRaises(Exception, action.run, repo_name="st2contrib") def test_run_repositories_blank(self): config = yaml.safe_load(MOCK_CONFIG_BLANK_REPOSITORIES) - action = ExpandRepoName(config) + action = self.get_action_instance(config=config) self.assertRaises(Exception, action.run, repo_name="st2contrib") def test_run_st2contrib_expands(self): config = yaml.safe_load(MOCK_CONFIG_FULL) - action = ExpandRepoName(config) + action = self.get_action_instance(config=config) expected = {'repo_url': 'https://github.com/StackStorm/st2contrib.git', 'subtree': True} @@ -55,7 +57,7 @@ def test_run_st2contrib_expands(self): def test_run_st2incubator_expands(self): config = yaml.safe_load(MOCK_CONFIG_FULL) - action = ExpandRepoName(config) + action = self.get_action_instance(config=config) expected = {'repo_url': 'https://github.com/StackStorm/st2incubator.git', 'subtree': True} diff --git a/st2actions/st2actions/runners/python_action_wrapper.py b/st2actions/st2actions/runners/python_action_wrapper.py index ce53949dc9..86c37b6c14 100644 --- a/st2actions/st2actions/runners/python_action_wrapper.py +++ b/st2actions/st2actions/runners/python_action_wrapper.py @@ -16,11 +16,12 @@ import sys import json import argparse -import logging as stdlib_logging from st2common import log as logging from st2actions import config from st2actions.runners.pythonrunner import Action +from st2actions.runners.utils import get_logger_for_python_runner_action +from st2actions.runners.utils import get_action_class_instance from st2common.util import loader as action_loader from st2common.util.config_parser import ContentPackConfigParser from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER @@ -28,12 +29,45 @@ from st2common.services.datastore import DatastoreService __all__ = [ - 'PythonActionWrapper' + 'PythonActionWrapper', + 'ActionService' ] LOG = logging.getLogger(__name__) +class ActionService(object): + """ + Instance of this class is passed to the action instance and exposes "public" + methods which can be called by the action. + """ + + def __init__(self, action_wrapper): + logger = get_logger_for_python_runner_action(action_name=action_wrapper._class_name) + + self._action_wrapper = action_wrapper + self._datastore_service = DatastoreService(logger=logger, + pack_name=self._action_wrapper._pack, + class_name=self._action_wrapper._class_name, + api_username='action_service') + + ################################## + # Methods for datastore management + ################################## + + def list_values(self, local=True, prefix=None): + return self._datastore_service.list_values(local, prefix) + + def get_value(self, name, local=True): + return self._datastore_service.get_value(name, local) + + def set_value(self, name, value, ttl=None, local=True): + return self._datastore_service.set_value(name, value, ttl, local) + + def delete_value(self, name, local=True): + return self._datastore_service.delete_value(name, local) + + class PythonActionWrapper(object): def __init__(self, pack, file_path, parameters=None, parent_args=None): """ @@ -92,39 +126,17 @@ def _get_action_instance(self): if config: LOG.info('Using config "%s" for action "%s"' % (config.file_path, self._file_path)) - - action_instance = action_cls(config=config.config) + config = config.config else: LOG.info('No config found for action "%s"' % (self._file_path)) - action_instance = action_cls(config={}) - - # Setup action_instance proeprties - action_instance.logger = self._set_up_logger(action_cls.__name__) - action_instance.datastore = DatastoreService(logger=action_instance.logger, - pack_name=self._pack, - class_name=action_cls.__name__, - api_username="action_service") + config = None + action_service = ActionService(action_wrapper=self) + action_instance = get_action_class_instance(action_cls=action_cls, + config=config, + action_service=action_service) return action_instance - def _set_up_logger(self, action_name): - """ - Set up a logger which logs all the messages with level DEBUG - and above to stderr. - """ - logger_name = 'actions.python.%s' % (action_name) - logger = logging.getLogger(logger_name) - - console = stdlib_logging.StreamHandler() - console.setLevel(stdlib_logging.DEBUG) - - formatter = stdlib_logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - logger.setLevel(stdlib_logging.DEBUG) - - return logger - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Python action runner process wrapper') diff --git a/st2actions/st2actions/runners/pythonrunner.py b/st2actions/st2actions/runners/pythonrunner.py index 4e8ab40e0a..328247c611 100644 --- a/st2actions/st2actions/runners/pythonrunner.py +++ b/st2actions/st2actions/runners/pythonrunner.py @@ -23,6 +23,7 @@ from eventlet.green import subprocess from st2actions.runners import ActionRunner +from st2actions.runners.utils import get_logger_for_python_runner_action from st2common.util.green.shell import run_command from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED @@ -63,15 +64,17 @@ class Action(object): description = None - def __init__(self, config=None): + def __init__(self, config=None, action_service=None): """ :param config: Action config. :type config: ``dict`` + + :param action_service: ActionService object. + :type action_service: :class:`ActionService~ """ self.config = config or {} - # logger and datastore are assigned in PythonActionWrapper._get_action_instance - self.logger = None - self.datastore = None + self.action_service = action_service + self.logger = get_logger_for_python_runner_action(action_name=self.__class__.__name__) @abc.abstractmethod def run(self, **kwargs): diff --git a/st2actions/st2actions/runners/utils.py b/st2actions/st2actions/runners/utils.py new file mode 100644 index 0000000000..b78be230ff --- /dev/null +++ b/st2actions/st2actions/runners/utils.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 logging as stdlib_logging + +from st2common import log as logging + +__all__ = [ + 'get_logger_for_python_runner_action', + 'get_action_class_instance' +] + +LOG = logging.getLogger(__name__) + + +def get_logger_for_python_runner_action(action_name): + """ + Set up a logger which logs all the messages with level DEBUG and above to stderr. + """ + logger_name = 'actions.python.%s' % (action_name) + logger = logging.getLogger(logger_name) + + console = stdlib_logging.StreamHandler() + console.setLevel(stdlib_logging.DEBUG) + + formatter = stdlib_logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + console.setFormatter(formatter) + logger.addHandler(console) + logger.setLevel(stdlib_logging.DEBUG) + + return logger + + +def get_action_class_instance(action_cls, config=None, action_service=None): + """ + Instantiate and return Action class instance. + + :param action_cls: Action class to instantiate. + :type action_cls: ``class`` + + :param config: Config to pass to the action class. + :type config: ``dict`` + + :param action_service: ActionService instance to pass to the class. + :type action_service: :class:`ActionService` + """ + kwargs = {} + kwargs['config'] = config + kwargs['action_service'] = action_service + + # Note: This is done for backward compatibility reasons. We first try to pass + # "action_service" argument to the action class constructor, but if that doesn't work (e.g. old + # action which hasn't been updated yet), we resort to late assignment post class instantiation. + # TODO: Remove in next major version once all the affected actions have been updated. + try: + action_instance = action_cls(**kwargs) + except TypeError as e: + if 'unexpected keyword argument \'action_service\'' not in str(e): + raise e + + LOG.debug('Action class (%s) constructor doesn\'t take "action_service" argument, ' + 'falling back to late assignment...' % (action_cls.__class__.__name__)) + + action_service = kwargs.pop('action_service', None) + action_instance = action_cls(**kwargs) + action_instance.action_service = action_service + + return action_instance diff --git a/st2actions/tests/unit/test_pythonrunner.py b/st2actions/tests/unit/test_pythonrunner.py index 7dc9cae8f7..eb3e4ba31e 100644 --- a/st2actions/tests/unit/test_pythonrunner.py +++ b/st2actions/tests/unit/test_pythonrunner.py @@ -18,7 +18,9 @@ import mock from st2actions.runners import pythonrunner +from st2actions.runners.pythonrunner import Action from st2actions.container import service +from st2actions.runners.utils import get_action_class_instance from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED, LIVEACTION_STATUS_FAILED from st2common.constants.pack import SYSTEM_PACK_NAME @@ -184,6 +186,49 @@ def test_common_st2_env_vars_are_available_to_the_action(self, mock_popen): actual_env = call_kwargs['env'] self.assertCommonSt2EnvVarsAvailableInEnv(env=actual_env) + def test_action_class_instantiation_action_service_argument(self): + class Action1(Action): + # Constructor not overriden so no issue here + pass + + def run(self): + pass + + class Action2(Action): + # Constructor overriden, but takes action_service argument + def __init__(self, config, action_service=None): + super(Action2, self).__init__(config=config, + action_service=action_service) + + def run(self): + pass + + class Action3(Action): + # Constructor overriden, but doesn't take to action service + def __init__(self, config): + super(Action3, self).__init__(config=config) + + def run(self): + pass + + config = {'a': 1, 'b': 2} + action_service = 'ActionService!' + + action1 = get_action_class_instance(action_cls=Action1, config=config, + action_service=action_service) + self.assertEqual(action1.config, config) + self.assertEqual(action1.action_service, action_service) + + action2 = get_action_class_instance(action_cls=Action2, config=config, + action_service=action_service) + self.assertEqual(action2.config, config) + self.assertEqual(action2.action_service, action_service) + + action3 = get_action_class_instance(action_cls=Action3, config=config, + action_service=action_service) + self.assertEqual(action3.config, config) + self.assertEqual(action3.action_service, action_service) + def _get_mock_action_obj(self): """ Return mock action object. diff --git a/st2common/tests/unit/test_unit_testing_mocks.py b/st2common/tests/unit/test_unit_testing_mocks.py index 250638dfda..90f46c132f 100644 --- a/st2common/tests/unit/test_unit_testing_mocks.py +++ b/st2common/tests/unit/test_unit_testing_mocks.py @@ -18,10 +18,13 @@ from st2tests.base import BaseSensorTestCase from st2tests.mocks.sensor import MockSensorWrapper from st2tests.mocks.sensor import MockSensorService +from st2tests.mocks.action import MockActionWrapper +from st2tests.mocks.action import MockActionService __all__ = [ 'BaseSensorTestCaseTestCase', - 'MockSensorServiceTestCase' + 'MockSensorServiceTestCase', + 'MockActionServiceTestCase' ] @@ -29,6 +32,38 @@ class MockSensorClass(object): pass +class BaseMockResourceServiceTestCase(object): + class TestCase(unittest2.TestCase): + def test_list_set_get_delete_values(self): + # list_values, set_value + result = self.mock_service.list_values() + self.assertSequenceEqual(result, []) + + self.mock_service.set_value(name='t1.local', value='test1', local=True) + self.mock_service.set_value(name='t1.global', value='test1', local=False) + + result = self.mock_service.list_values(local=True) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].name, 'dummy.test:t1.local') + + result = self.mock_service.list_values(local=False) + self.assertEqual(result[0].name, 'dummy.test:t1.local') + self.assertEqual(result[1].name, 't1.global') + self.assertEqual(len(result), 2) + + # get_value + self.assertEqual(self.mock_service.get_value('inexistent'), None) + self.assertEqual(self.mock_service.get_value(name='t1.local', local=True), 'test1') + + # delete_value + self.assertEqual(len(self.mock_service.list_values(local=True)), 1) + self.assertEqual(self.mock_service.delete_value('inexistent'), False) + self.assertEqual(len(self.mock_service.list_values(local=True)), 1) + + self.assertEqual(self.mock_service.delete_value('t1.local'), True) + self.assertEqual(len(self.mock_service.list_values(local=True)), 0) + + class BaseSensorTestCaseTestCase(BaseSensorTestCase): sensor_cls = MockSensorClass @@ -51,12 +86,14 @@ def test_dispatch_and_assertTriggerDispacthed(self): payload={'a': 'c'}) -class MockSensorServiceTestCase(unittest2.TestCase): +class MockSensorServiceTestCase(BaseMockResourceServiceTestCase.TestCase): + def setUp(self): - self._mock_sensor_wrapper = MockSensorWrapper(pack='dummy', class_name='test') + mock_sensor_wrapper = MockSensorWrapper(pack='dummy', class_name='test') + self.mock_service = MockSensorService(sensor_wrapper=mock_sensor_wrapper) def test_get_logger(self): - sensor_service = MockSensorService(sensor_wrapper=self._mock_sensor_wrapper) + sensor_service = self.mock_service logger = sensor_service.get_logger('test') logger.info('test info') logger.debug('test debug') @@ -73,33 +110,8 @@ def test_get_logger(self): self.assertEqual(method_args, ('test debug',)) self.assertEqual(method_kwargs, {}) - def test_list_set_get_delete_values(self): - sensor_service = MockSensorService(sensor_wrapper=self._mock_sensor_wrapper) - - # list_values, set_value - result = sensor_service.list_values() - self.assertSequenceEqual(result, []) - - sensor_service.set_value(name='t1.local', value='test1', local=True) - sensor_service.set_value(name='t1.global', value='test1', local=False) - - result = sensor_service.list_values(local=True) - self.assertEqual(len(result), 1) - self.assertEqual(result[0].name, 'dummy.test:t1.local') - result = sensor_service.list_values(local=False) - self.assertEqual(result[0].name, 'dummy.test:t1.local') - self.assertEqual(result[1].name, 't1.global') - self.assertEqual(len(result), 2) - - # get_value - self.assertEqual(sensor_service.get_value('inexistent'), None) - self.assertEqual(sensor_service.get_value(name='t1.local', local=True), 'test1') - - # delete_value - self.assertEqual(len(sensor_service.list_values(local=True)), 1) - self.assertEqual(sensor_service.delete_value('inexistent'), False) - self.assertEqual(len(sensor_service.list_values(local=True)), 1) - - self.assertEqual(sensor_service.delete_value('t1.local'), True) - self.assertEqual(len(sensor_service.list_values(local=True)), 0) +class MockActionServiceTestCase(BaseMockResourceServiceTestCase.TestCase): + def setUp(self): + mock_action_wrapper = MockActionWrapper(pack='dummy', class_name='test') + self.mock_service = MockActionService(action_wrapper=mock_action_wrapper) diff --git a/st2reactor/st2reactor/container/sensor_wrapper.py b/st2reactor/st2reactor/container/sensor_wrapper.py index 2bd873f33a..114da6b80c 100644 --- a/st2reactor/st2reactor/container/sensor_wrapper.py +++ b/st2reactor/st2reactor/container/sensor_wrapper.py @@ -35,7 +35,8 @@ from st2common.services.datastore import DatastoreService __all__ = [ - 'SensorWrapper' + 'SensorWrapper', + 'SensorService' ] eventlet.monkey_patch( diff --git a/st2tests/st2tests/base.py b/st2tests/st2tests/base.py index 96a17fba5b..992d190234 100644 --- a/st2tests/st2tests/base.py +++ b/st2tests/st2tests/base.py @@ -46,10 +46,13 @@ import st2common.models.db.liveaction as liveaction_model import st2common.models.db.actionalias as actionalias_model import st2common.models.db.policy as policy_model +from st2actions.runners.utils import get_action_class_instance import st2tests.config from st2tests.mocks.sensor import MockSensorWrapper from st2tests.mocks.sensor import MockSensorService +from st2tests.mocks.action import MockActionWrapper +from st2tests.mocks.action import MockActionService __all__ = [ @@ -490,6 +493,23 @@ class BaseActionTestCase(TestCase): action_cls = None + def setUp(self): + super(BaseActionTestCase, self).setUp() + + class_name = self.action_cls.__name__ + action_wrapper = MockActionWrapper(pack='tests', class_name=class_name) + self.action_service = MockActionService(action_wrapper=action_wrapper) + + def get_action_instance(self, config=None): + """ + Retrieve instance of the action class. + """ + # pylint: disable=not-callable + instance = get_action_class_instance(action_cls=self.action_cls, + config=config, + action_service=self.action_service) + return instance + class FakeResponse(object): diff --git a/st2tests/st2tests/mocks/action.py b/st2tests/st2tests/mocks/action.py new file mode 100644 index 0000000000..1fda11f547 --- /dev/null +++ b/st2tests/st2tests/mocks/action.py @@ -0,0 +1,54 @@ +# 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. + +""" +Mock classes for use in pack testing. +""" + +from logging import RootLogger + +from mock import Mock + +from st2actions.runners.python_action_wrapper import ActionService +from st2tests.mocks.datastore import MockDatastoreService + +__all__ = [ + 'MockActionWrapper', + 'MockActionService' +] + + +class MockActionWrapper(object): + def __init__(self, pack, class_name): + self._pack = pack + self._class_name = class_name + + +class MockActionService(ActionService): + """ + Mock ActionService for use in testing. + """ + + def __init__(self, action_wrapper): + self._action_wrapper = action_wrapper + + # Holds a mock logger instance + # We use a Mock class so use can assert logger was called with particular arguments + self._logger = Mock(spec=RootLogger) + + self._datastore_service = MockDatastoreService(logger=self._logger, + pack_name=self._action_wrapper._pack, + class_name=self._action_wrapper._class_name, + api_username='action_service')