From 4c5f6dcad4ccbc5d8c05129547a0dbe3ee81f9ba Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Mon, 8 Feb 2021 22:17:33 +0100 Subject: [PATCH 1/7] Add new "st2 action-alias execute-and-format" command which allows user to execute an action alias and render the result and display it. This will display the same message which would otherwise have been displayed in chat. This should make testing various aliases easier and faster. --- st2client/st2client/commands/action_alias.py | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/st2client/st2client/commands/action_alias.py b/st2client/st2client/commands/action_alias.py index a9a15e7bed..1988287169 100644 --- a/st2client/st2client/commands/action_alias.py +++ b/st2client/st2client/commands/action_alias.py @@ -16,9 +16,11 @@ from __future__ import absolute_import from st2client.models import core +from st2client.models.action import Execution from st2client.models.action_alias import ActionAlias from st2client.models.action_alias import ActionAliasMatch from st2client.commands import resource +from st2client.commands.action import ActionRunCommandMixin from st2client.formatters import table @@ -45,6 +47,9 @@ def __init__(self, description, app, subparsers, parent_parser=None): self.commands['execute'] = ActionAliasExecuteCommand( self.resource, self.app, self.subparsers, add_help=True) + self.commands['execute-and-format'] = ActionAliasExecuteAndFormatCommand( + self.resource, self.app, self.subparsers, + add_help=True) class ActionAliasListCommand(resource.ContentPackResourceListCommand): @@ -127,3 +132,87 @@ def run_and_print(self, args, **kwargs): print("Matching Action-alias: '%s'" % execution.actionalias['ref']) print("To get the results, execute:\n st2 execution get %s" % (execution.execution['id'])) + + +class ActionAliasExecuteAndFormatCommand(ActionRunCommandMixin, resource.ResourceCommand): + display_attributes = ['name'] + + def __init__(self, resource, *args, **kwargs): + super(ActionAliasExecuteAndFormatCommand, self).__init__( + resource, 'execute-and-format', + ('Execute the command text by finding a matching %s and format the result.' % + resource.get_display_name().lower()), *args, **kwargs) + + self.parser.add_argument('command_text', + metavar='command', + help=('Execute the command text by finding a matching %s.' % + resource.get_display_name().lower())) + self.parser.add_argument('-u', '--user', type=str, default=None, + help='User under which to run the action (admins only).') + + self._add_common_options() + self.parser.add_argument('-a', '--async', + action='store_true', dest='action_async', + help='Do not wait for action to finish.') + + @resource.add_auth_token_to_kwargs_from_cli + def run(self, args, **kwargs): + payload = core.Resource() + payload.command = args.command_text + payload.user = args.user or "" + payload.source_channel = 'cli' + + alias_execution_mgr = self.app.client.managers['ActionAliasExecution'] + execution = alias_execution_mgr.match_and_execute(payload) + return execution + + def run_and_print(self, args, **kwargs): + # 1. Trigger the execution via alias + print("Triggering execution via action alias") + print("") + + # NOTE: This will return an error and abort if command matches no aliases so no additional + # checks are needed + result = self.run(args, **kwargs) + execution = Execution.deserialize(result.execution) + + # 2. Wait for it to complete + print("Execution (%s) has been started, waiting for it to finish..." % (execution.id)) + print("") + + action_exec_mgr = self.app.client.managers['Execution'] + execution = self._get_execution_result(execution=execution, + action_exec_mgr=action_exec_mgr, + args=args, **kwargs) + execution_id = execution.id + + # 3. Run chatops.format_result action with the result of the completed execution + print("") + print("Execution (%s) has finished, rendering result..." % (execution_id)) + print("") + + format_execution = Execution() + format_execution.action = "chatops.format_execution_result" + format_execution.parameters = { + "execution_id": execution_id + } + format_execution.user = args.user or "" + + format_execution = action_exec_mgr.create(format_execution, **kwargs) + + print("Execution (%s) has been started, waiting for it to finish..." % ( + format_execution.id)) + print("") + + # 4. Wait for chatops.format_execution_result to finish and print the result + format_execution = self._get_execution_result(execution=format_execution, + action_exec_mgr=action_exec_mgr, + args=args, **kwargs) + + print("") + print("Formatted ChatOps result message") + print("") + print("=" * 80) + print(format_execution.result["result"]["message"]) + print("=" * 80) + print("") From 53885659b385f2ddb1ea96ec592985cc28d77a22 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 12 Feb 2021 00:29:37 +0100 Subject: [PATCH 2/7] Pin to a fixed version. --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 6ca0e9608d..430d9f6eb9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ pep8==1.7.1 st2flake8==0.1.0 astroid==2.4.2 pylint==2.6.0 -pylint-plugin-utils>=0.4 +pylint-plugin-utils==0.6 bandit==1.5.1 ipython<6.0.0 isort>=4.2.5 From 2f78a21a63454b627bc24b761a3309d14aac2644 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Fri, 12 Feb 2021 11:18:47 +0100 Subject: [PATCH 3/7] Rename command to st2 action-alias test, add changelog entry. --- CHANGELOG.rst | 14 +++++++++++++- st2client/st2client/commands/action_alias.py | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 94db66b6db..5ea5f00f46 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,12 +12,24 @@ Added * Added st2-rbac-backend pip requirements for RBAC integration. (new feature) #5086 Contributed by @hnanchahal - + * Added notification support for err-stackstorm. (new feature) #5051 * Added st2-auth-ldap pip requirements for LDAP auth integartion. (new feature) #5082 Contributed by @hnanchahal +* Add new ``st2 action-alias test `` CLI command which allows users to easily + test action alias matching and result formatting. + + This command will first try to find a matching alias (same as ``st2 action-alias match`` + command) and if a match is found, trigger an execution (same as ``st2 action-alias execute`` + command) and format the execution result. + + This means it uses exactly the same flow as commands on chat, but the interaction avoids + chat and hubot which should make testing and developing aliases easier and faster. #5143 + + Contributed by @Kami. + Changed ~~~~~~~~~ * Updated deprecation warning for python 2 pack installs, following python 2 support removal. #5099 diff --git a/st2client/st2client/commands/action_alias.py b/st2client/st2client/commands/action_alias.py index 1988287169..a7ada0039a 100644 --- a/st2client/st2client/commands/action_alias.py +++ b/st2client/st2client/commands/action_alias.py @@ -47,7 +47,7 @@ def __init__(self, description, app, subparsers, parent_parser=None): self.commands['execute'] = ActionAliasExecuteCommand( self.resource, self.app, self.subparsers, add_help=True) - self.commands['execute-and-format'] = ActionAliasExecuteAndFormatCommand( + self.commands['test'] = ActionAliasTestCommand( self.resource, self.app, self.subparsers, add_help=True) @@ -134,12 +134,12 @@ def run_and_print(self, args, **kwargs): (execution.execution['id'])) -class ActionAliasExecuteAndFormatCommand(ActionRunCommandMixin, resource.ResourceCommand): +class ActionAliasTestCommand(ActionRunCommandMixin, resource.ResourceCommand): display_attributes = ['name'] def __init__(self, resource, *args, **kwargs): - super(ActionAliasExecuteAndFormatCommand, self).__init__( - resource, 'execute-and-format', + super(ActionAliasTestCommand, self).__init__( + resource, 'test', ('Execute the command text by finding a matching %s and format the result.' % resource.get_display_name().lower()), *args, **kwargs) From 74ca3470c7fb634121c102ffb07b03c44ec92366 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Sat, 13 Feb 2021 17:42:04 +0100 Subject: [PATCH 4/7] Add basic mock based tests for the new CLI command. --- st2client/st2client/commands/action.py | 18 ++++- st2client/st2client/commands/action_alias.py | 4 +- st2client/tests/unit/test_action_alias.py | 72 +++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/st2client/st2client/commands/action.py b/st2client/st2client/commands/action.py index 7a41d9e2eb..f555070aad 100644 --- a/st2client/st2client/commands/action.py +++ b/st2client/st2client/commands/action.py @@ -447,7 +447,14 @@ def _run_and_print_child_task_list(self, execution, args, **kwargs): yaml=args.yaml, attribute_transform_functions=self.attribute_transform_functions) - def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): + def _get_execution_result(self, execution, action_exec_mgr, args, + force_retry_on_finish=False, **kwargs): + """ + :param force_retry_on_finish: True to retry execution details on finish even if the + execution which is passed to this method has already finished. + This ensures we have latest state available for that + execution. + """ pending_statuses = [ LIVEACTION_STATUS_REQUESTED, LIVEACTION_STATUS_SCHEDULED, @@ -469,8 +476,11 @@ def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): print('') return execution + poll_counter = 0 + if not args.action_async: while execution.status in pending_statuses: + poll_counter += 1 time.sleep(self.poll_interval) if not args.json and not args.yaml: sys.stdout.write('.') @@ -479,6 +489,12 @@ def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs): sys.stdout.write('\n') + if poll_counter == 0 and force_retry_on_finish: + # In some situations we want to retry execution details from API + # even if it has already finished before performing even a single poll. This ensures + # we have the latest data for a particular execution. + execution = action_exec_mgr.get_by_id(execution.id, **kwargs) + if execution.status == LIVEACTION_STATUS_CANCELED: return execution diff --git a/st2client/st2client/commands/action_alias.py b/st2client/st2client/commands/action_alias.py index 2c664d4737..58b160d984 100644 --- a/st2client/st2client/commands/action_alias.py +++ b/st2client/st2client/commands/action_alias.py @@ -207,7 +207,9 @@ def run_and_print(self, args, **kwargs): # 4. Wait for chatops.format_execution_result to finish and print the result format_execution = self._get_execution_result(execution=format_execution, action_exec_mgr=action_exec_mgr, - args=args, **kwargs) + args=args, + force_retry_on_finish=True, + **kwargs) print("") print("Formatted ChatOps result message") diff --git a/st2client/tests/unit/test_action_alias.py b/st2client/tests/unit/test_action_alias.py index a360fd5139..8823fa7b9b 100644 --- a/st2client/tests/unit/test_action_alias.py +++ b/st2client/tests/unit/test_action_alias.py @@ -21,9 +21,10 @@ from tests import base from st2client import shell +from st2client import models from st2client.utils import httpclient -MOCK_MATCH_AND_EXECUTE_RESULT = { +MOCK_MATCH_AND_EXECUTE_RESULT_1 = { "results": [ { "execution": { @@ -36,6 +37,33 @@ ] } +MOCK_MATCH_AND_EXECUTE_RESULT_2 = { + "results": [ + { + "execution": { + "id": "mock-id-execute", + "status": "succeeded" + }, + "actionalias": { + "ref": "mock-ref" + }, + "liveaction": { + "id": "mock-id", + } + } + ] +} + +MOCK_CREATE_EXECUTION_RESULT = { + "id": "mock-id-format-execution", + "status": "succeeded", + "result": { + "result": { + "message": "Result formatted message" + } + } +} + class ActionAliasCommandTestCase(base.BaseCLITestCase): def __init__(self, *args, **kwargs): @@ -44,9 +72,9 @@ def __init__(self, *args, **kwargs): @mock.patch.object( httpclient.HTTPClient, 'post', - mock.MagicMock(return_value=base.FakeResponse(json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT), + mock.MagicMock(return_value=base.FakeResponse(json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT_1), 200, 'OK'))) - def test_match_and_execute(self): + def test_match_and_execute_success(self): ret = self.shell.run(['action-alias', 'execute', "run whoami on localhost"]) self.assertEqual(ret, 0) @@ -62,3 +90,41 @@ def test_match_and_execute(self): self.assertTrue("Matching Action-alias: 'mock-ref'" in mock_stdout) self.assertTrue("st2 execution get mock-id" in mock_stdout) + + @mock.patch.object( + httpclient.HTTPClient, 'post', + mock.MagicMock(side_effect=[ + base.FakeResponse(json.dumps(MOCK_MATCH_AND_EXECUTE_RESULT_2), 200, 'OK'), + base.FakeResponse(json.dumps(MOCK_CREATE_EXECUTION_RESULT), 200, 'OK') + ])) + @mock.patch.object( + models.ResourceManager, 'get_by_id', + mock.MagicMock(return_value=models.Execution(**MOCK_CREATE_EXECUTION_RESULT))) + def test_test_command_success(self): + ret = self.shell.run(['action-alias', 'test', "run whoami on localhost"]) + self.assertEqual(ret, 0) + + expected_args = { + 'command': 'run whoami on localhost', + 'user': '', + 'source_channel': 'cli' + } + httpclient.HTTPClient.post.assert_any_call('/aliasexecution/match_and_execute', + expected_args) + + expected_args = { + 'action': 'chatops.format_execution_result', + 'parameters': {'execution_id': 'mock-id-execute'}, + 'user': '' + } + httpclient.HTTPClient.post.assert_any_call('/executions', + expected_args) + + mock_stdout = self.stdout.getvalue() + + self.assertTrue("Execution (mock-id-execute) has been started, waiting for it to finish" in + mock_stdout) + self.assertTrue("Execution (mock-id-format-execution) has been started, waiting for it to " + "finish" in mock_stdout) + self.assertTrue("Formatted ChatOps result message" in mock_stdout) + self.assertTrue("Result formatted message" in mock_stdout) From 473667e0bce048c4ee673b8a1a33c980a07fb265 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Sun, 2 May 2021 13:14:14 +0200 Subject: [PATCH 5/7] Fix typo. --- st2client/st2client/commands/action.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/st2client/st2client/commands/action.py b/st2client/st2client/commands/action.py index 14ed1c21c5..fc8d98741d 100644 --- a/st2client/st2client/commands/action.py +++ b/st2client/st2client/commands/action.py @@ -632,9 +632,9 @@ def _get_execution_result( sys.stdout.write("\n") if poll_counter == 0 and force_retry_on_finish: - # In some situations we want to retry execution details from API - # even if it has already finished before performing even a single poll. This ensures - # we have the latest data for a particular execution. + # In some situations we want to retrieve execution details from API even if it has + # already finished before performing even a single poll. This ensures we have the + # latest data for a particular execution. execution = action_exec_mgr.get_by_id(execution.id, **kwargs) if execution.status == LIVEACTION_STATUS_CANCELED: From c0e01686840009619af2bfad5567388cdbc0c92e Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Sun, 2 May 2021 13:14:52 +0200 Subject: [PATCH 6/7] Update st2client/st2client/commands/action_alias.py Co-authored-by: blag --- st2client/st2client/commands/action_alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/st2client/st2client/commands/action_alias.py b/st2client/st2client/commands/action_alias.py index 8560c2bd1b..d32c39af55 100644 --- a/st2client/st2client/commands/action_alias.py +++ b/st2client/st2client/commands/action_alias.py @@ -256,7 +256,7 @@ def run_and_print(self, args, **kwargs): # 3. Run chatops.format_result action with the result of the completed execution print("") - print("Execution (%s) has finished, rendering result..." % (execution_id)) + print(f"Execution ({execution_id}) has finished, rendering result...") print("") format_execution = Execution() From 83d88c923e588212481055cb1d55cd2aa479bf9a Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 11 Nov 2021 08:31:22 -0600 Subject: [PATCH 7/7] Use explicit imports for st2client models Follow code style introduced in #5333 --- st2client/tests/unit/test_action_alias.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/st2client/tests/unit/test_action_alias.py b/st2client/tests/unit/test_action_alias.py index 75b90b2661..07f6682d77 100644 --- a/st2client/tests/unit/test_action_alias.py +++ b/st2client/tests/unit/test_action_alias.py @@ -21,7 +21,8 @@ from tests import base from st2client import shell -from st2client import models +from st2client.models.core import ResourceManager +from st2client.models.action import Execution from st2client.utils import httpclient MOCK_MATCH_AND_EXECUTE_RESULT_1 = { @@ -99,9 +100,9 @@ def test_match_and_execute_success(self): ), ) @mock.patch.object( - models.ResourceManager, + ResourceManager, "get_by_id", - mock.MagicMock(return_value=models.Execution(**MOCK_CREATE_EXECUTION_RESULT)), + mock.MagicMock(return_value=Execution(**MOCK_CREATE_EXECUTION_RESULT)), ) def test_test_command_success(self): ret = self.shell.run(["action-alias", "test", "run whoami on localhost"])