From 584f39d36f0adc9464d6b4d1d923527cba4a84fe Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 21 Oct 2022 17:34:46 -0700 Subject: [PATCH 1/5] switching the slack_engine to use slack_bolt --- salt/engines/slack.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/salt/engines/slack.py b/salt/engines/slack.py index 54c905030c48..a7543281c2f0 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -166,11 +166,12 @@ import salt.utils.yaml try: - import slackclient + from slack_bolt import App + from slack_bolt.adapter.socket_mode import SocketModeHandler - HAS_SLACKCLIENT = True + HAS_SLACKBOLT = True except ImportError: - HAS_SLACKCLIENT = False + HAS_SLACKBOLT = False log = logging.getLogger(__name__) @@ -178,17 +179,20 @@ def __virtual__(): - if not HAS_SLACKCLIENT: + if not HAS_SLACKBOLT: return (False, "The 'slackclient' Python module could not be loaded") return __virtualname__ class SlackClient: - def __init__(self, token): + def __init__(self, app_token, bot_token): self.master_minion = salt.minion.MasterMinion(__opts__) - self.sc = slackclient.SlackClient(token) - self.slack_connect = self.sc.rtm_connect() + self.app = App(token=bot_token) + self.handler = SocketModeHandler(self.app, app_token) + self.handler.connect() + + self.app.message(re.compile("(^!.*)"))(self.message_trigger) def get_slack_users(self, token): """ @@ -932,11 +936,13 @@ def start( log.error("Slack bot token not found, bailing...") raise UserWarning("Slack Engine bot token not configured") + app_token = "xapp-1-A047F7H80DC-4245337892359-e26770884d0e159372cdeb768fa44ca62523f144c7082d4d56d76127e3619456" + bot_token = "xoxb-2848035968-4245469590103-ZE5uptNNYhffMiM8rND5iX01" try: - client = SlackClient(token=token) - message_generator = client.generate_triggered_messages( - token, trigger, groups, groups_pillar_name - ) - client.run_commands_from_slack_async(message_generator, fire_all, tag, control) + client = SlackClient(app_token=app_token, bot_token=bot_token) + # message_generator = client.generate_triggered_messages( + # token, trigger, groups, groups_pillar_name + # ) + # client.run_commands_from_slack_async(message_generator, fire_all, tag, control) except Exception: # pylint: disable=broad-except raise Exception("{}".format(traceback.format_exc())) From 82e12b3402272623f20d093835428c08c58ae77c Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 25 Oct 2022 14:46:58 -0700 Subject: [PATCH 2/5] Some more cleanup. Using collections.deque to gather up the messages from Slack before they are handled. --- changelog/57842.fixed | 1 + salt/engines/slack.py | 159 +++++++++++++---------- salt/utils/slack.py | 2 +- tests/pytests/unit/engines/test_slack.py | 40 +++++- 4 files changed, 126 insertions(+), 76 deletions(-) create mode 100644 changelog/57842.fixed diff --git a/changelog/57842.fixed b/changelog/57842.fixed new file mode 100644 index 000000000000..c708020bd1ae --- /dev/null +++ b/changelog/57842.fixed @@ -0,0 +1 @@ +Updating Slack engine to use slack_bolt library. diff --git a/salt/engines/slack.py b/salt/engines/slack.py index a7543281c2f0..0d8abb73781f 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -146,6 +146,7 @@ """ import ast +import collections import datetime import itertools import logging @@ -166,8 +167,8 @@ import salt.utils.yaml try: - from slack_bolt import App - from slack_bolt.adapter.socket_mode import SocketModeHandler + import slack_bolt + import slack_bolt.adapter.socket_mode HAS_SLACKBOLT = True except ImportError: @@ -180,19 +181,34 @@ def __virtual__(): if not HAS_SLACKBOLT: - return (False, "The 'slackclient' Python module could not be loaded") + return (False, "The 'slack_bolt' Python module could not be loaded") return __virtualname__ class SlackClient: - def __init__(self, app_token, bot_token): + def __init__(self, app_token, bot_token, trigger_string): self.master_minion = salt.minion.MasterMinion(__opts__) - self.app = App(token=bot_token) - self.handler = SocketModeHandler(self.app, app_token) + self.app = slack_bolt.App(token=bot_token) + self.handler = slack_bolt.adapter.socket_mode.SocketModeHandler( + self.app, app_token + ) self.handler.connect() - self.app.message(re.compile("(^!.*)"))(self.message_trigger) + self.app_token = app_token + self.bot_token = bot_token + + self.msg_queue = collections.deque() + + trigger_pattern = "(^{}.*)".format(trigger_string) + + # Register message_trigger when we see messages that start + # with the trigger string + self.app.message(re.compile(trigger_pattern))(self.message_trigger) + + def message_trigger(self, message): + # Add the received message to the queue + self.msg_queue.append(message) def get_slack_users(self, token): """ @@ -547,13 +563,12 @@ def just_data(m_data): return data for sleeps in (5, 10, 30, 60): - if self.slack_connect: + if self.handler: break else: # see https://api.slack.com/docs/rate-limits log.warning( - "Slack connection is invalid. Server: %s, sleeping %s", - self.sc.server, + "Slack connection is invalid, sleeping %s", sleeps, ) time.sleep( @@ -562,51 +577,51 @@ def just_data(m_data): else: raise UserWarning( "Connection to slack is still invalid, giving up: {}".format( - self.slack_connect + self.handler ) ) # Boom! while True: - msg = self.sc.rtm_read() - for m_data in msg: + while self.msg_queue: + msg = self.msg_queue.popleft() try: - msg_text = self.message_text(m_data) + msg_text = self.message_text(msg) except (ValueError, TypeError) as msg_err: log.debug( "Got an error from trying to get the message text %s", msg_err ) - yield {"message_data": m_data} # Not a message type from the API? + yield {"message_data": msg} # Not a message type from the API? continue # Find the channel object from the channel name - channel = self.sc.server.channels.find(m_data["channel"]) - data = just_data(m_data) + channel = msg["channel"] + data = just_data(msg) if msg_text.startswith(trigger_string): loaded_groups = self.get_config_groups(groups, groups_pillar_name) if not data.get("user_name"): log.error( "The user %s can not be looked up via slack. What has" " happened here?", - m_data.get("user"), + msg.get("user"), ) channel.send_message( "The user {} can not be looked up via slack. Not" " running {}".format(data["user_id"], msg_text) ) - yield {"message_data": m_data} + yield {"message_data": msg} continue (allowed, target, cmdline) = self.control_message_target( data["user_name"], msg_text, loaded_groups, trigger_string ) - log.debug("Got target: %s, cmdline: %s", target, cmdline) if allowed: - yield { - "message_data": m_data, - "channel": m_data["channel"], + ret = { + "message_data": msg, + "channel": msg["channel"], "user": data["user_id"], "user_name": data["user_name"], "cmdline": cmdline, "target": target, } + yield ret continue else: channel.send_message( @@ -780,39 +795,42 @@ def run_commands_from_slack_async( # Drain the slack messages, up to 10 messages at a clip count = 0 for msg in message_generator: - # The message_generator yields dicts. Leave this loop - # on a dict that looks like {'done': True} or when we've done it - # 10 times without taking a break. - log.trace("Got a message from the generator: %s", msg.keys()) - if count > 10: - log.warning( - "Breaking in getting messages because count is exceeded" - ) - break - if not msg: - count += 1 - log.warning("Skipping an empty message.") - continue # This one is a dud, get the next message - if msg.get("done"): - log.trace("msg is done") - break - if fire_all: - log.debug("Firing message to the bus with tag: %s", tag) - log.debug("%s %s", tag, msg) - self.fire("{}/{}".format(tag, msg["message_data"].get("type")), msg) - if control and (len(msg) > 1) and msg.get("cmdline"): - channel = self.sc.server.channels.find(msg["channel"]) - jid = self.run_command_async(msg) - log.debug("Submitted a job and got jid: %s", jid) - outstanding[ - jid - ] = msg # record so we can return messages to the caller - channel.send_message( - "@{}'s job is submitted as salt jid {}".format( + if msg: + # The message_generator yields dicts. Leave this loop + # on a dict that looks like {'done': True} or when we've done it + # 10 times without taking a break. + log.trace("Got a message from the generator: %s", msg.keys()) + if count > 10: + log.warning( + "Breaking in getting messages because count is exceeded" + ) + break + if not msg: + count += 1 + log.warning("Skipping an empty message.") + continue # This one is a dud, get the next message + if msg.get("done"): + log.trace("msg is done") + break + if fire_all: + log.debug("Firing message to the bus with tag: %s", tag) + log.debug("%s %s", tag, msg) + self.fire( + "{}/{}".format(tag, msg["message_data"].get("type")), msg + ) + if control and (len(msg) > 1) and msg.get("cmdline"): + jid = self.run_command_async(msg) + log.debug("Submitted a job and got jid: %s", jid) + outstanding[ + jid + ] = msg # record so we can return messages to the caller + text_msg = "@{}'s job is submitted as salt jid {}".format( msg["user_name"], jid ) - ) - count += 1 + self.app.client.chat_postMessage( + channel=msg["channel"], text=text_msg + ) + count += 1 start_time = time.time() job_status = self.get_jobs_from_runner( outstanding.keys() @@ -829,7 +847,7 @@ def run_commands_from_slack_async( log.debug("ret to send back is %s", result) # formatting function? this_job = outstanding[jid] - channel = self.sc.server.channels.find(this_job["channel"]) + channel = this_job["channel"] return_text = self.format_return_text(result, function) return_prefix = ( "@{}'s job `{}` (id: {}) (target: {}) returned".format( @@ -839,19 +857,19 @@ def run_commands_from_slack_async( this_job["target"], ) ) - channel.send_message(return_prefix) + self.app.client.chat_postMessage( + channel=channel, text=return_prefix + ) ts = time.time() st = datetime.datetime.fromtimestamp(ts).strftime("%Y%m%d%H%M%S%f") filename = "salt-results-{}.yaml".format(st) - r = self.sc.api_call( - "files.upload", - channels=channel.id, + resp = self.app.client.files_upload( + channels=channel, filename=filename, content=return_text, ) # Handle unicode return - log.debug("Got back %s via the slack client", r) - resp = salt.utils.yaml.safe_load(salt.utils.json.dumps(r)) + log.debug("Got back %s via the slack client", resp) if "ok" in resp and resp["ok"] is False: this_job["channel"].send_message( "Error: {}".format(resp["error"]) @@ -919,7 +937,8 @@ def run_command_async(self, msg): def start( - token, + app_token, + bot_token, control=False, trigger="!", groups=None, @@ -931,18 +950,18 @@ def start( Listen to slack events and forward them to salt, new version """ - if (not token) or (not token.startswith("xoxb")): + if (not bot_token) or (not bot_token.startswith("xoxb")): time.sleep(2) # don't respawn too quickly log.error("Slack bot token not found, bailing...") raise UserWarning("Slack Engine bot token not configured") - app_token = "xapp-1-A047F7H80DC-4245337892359-e26770884d0e159372cdeb768fa44ca62523f144c7082d4d56d76127e3619456" - bot_token = "xoxb-2848035968-4245469590103-ZE5uptNNYhffMiM8rND5iX01" try: - client = SlackClient(app_token=app_token, bot_token=bot_token) - # message_generator = client.generate_triggered_messages( - # token, trigger, groups, groups_pillar_name - # ) - # client.run_commands_from_slack_async(message_generator, fire_all, tag, control) + client = SlackClient( + app_token=app_token, bot_token=bot_token, trigger_string=trigger + ) + message_generator = client.generate_triggered_messages( + bot_token, trigger, groups, groups_pillar_name + ) + client.run_commands_from_slack_async(message_generator, fire_all, tag, control) except Exception: # pylint: disable=broad-except raise Exception("{}".format(traceback.format_exc())) diff --git a/salt/utils/slack.py b/salt/utils/slack.py index 81a29da51495..74b98af46d3d 100644 --- a/salt/utils/slack.py +++ b/salt/utils/slack.py @@ -46,7 +46,7 @@ def query( ret = {"message": "", "res": True} slack_functions = { - "rooms": {"request": "channels.list", "response": "channels"}, + "rooms": {"request": "conversations.list", "response": "channels"}, "users": {"request": "users.list", "response": "members"}, "message": {"request": "chat.postMessage", "response": "channel"}, } diff --git a/tests/pytests/unit/engines/test_slack.py b/tests/pytests/unit/engines/test_slack.py index c4946b51a152..5fc486b9c910 100644 --- a/tests/pytests/unit/engines/test_slack.py +++ b/tests/pytests/unit/engines/test_slack.py @@ -9,11 +9,33 @@ pytestmark = [ pytest.mark.skipif( - slack.HAS_SLACKCLIENT is False, reason="The SlackClient is not installed" + slack.HAS_SLACKBOLT is False, reason="The slack_bolt is not installed" ) ] +class MockSlackBoltSocketMode: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + def connect(self, *args, **kwargs): + return True + + +class MockSlackBoltApp: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + self.client = None + self.logger = None + self.proxy = None + + def message(self, *args, **kwargs): + return MagicMock(return_value=True) + + @pytest.fixture def configure_loader_modules(): return {slack: {}} @@ -22,12 +44,20 @@ def configure_loader_modules(): @pytest.fixture def slack_client(): mock_opts = salt.config.DEFAULT_MINION_OPTS.copy() - token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx" + app_token = "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + bot_token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx" + trigger = "!" with patch.dict(slack.__opts__, mock_opts): - with patch("slackclient.SlackClient.rtm_connect", MagicMock(return_value=True)): - slack_client = slack.SlackClient(token) - yield slack_client + with patch( + "slack_bolt.App", MagicMock(autospec=True, return_value=MockSlackBoltApp()) + ): + with patch( + "slack_bolt.adapter.socket_mode.SocketModeHandler", + MagicMock(autospec=True, return_value=MockSlackBoltSocketMode()), + ): + slack_client = slack.SlackClient(app_token, bot_token, trigger) + yield slack_client def test_control_message_target(slack_client): From 93caca405832e92a78db43f0008ae449ef4c1102 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 25 Oct 2022 16:35:36 -0700 Subject: [PATCH 3/5] Updating setup instructions to follow those for setting up an App. --- salt/engines/slack.py | 57 +++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/salt/engines/slack.py b/salt/engines/slack.py index 0d8abb73781f..a079781f1f5d 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -3,21 +3,43 @@ .. versionadded:: 2016.3.0 -:depends: `slackclient `_ Python module +:depends: `slack_bolt `_ Python module .. important:: - This engine requires a bot user. To create a bot user, first go to the - **Custom Integrations** page in your Slack Workspace. Copy and paste the - following URL, and replace ``myworkspace`` with the proper value for your - workspace: - - ``https://myworkspace.slack.com/apps/manage/custom-integrations`` - - Next, click on the ``Bots`` integration and request installation. Once - approved by an admin, you will be able to proceed with adding the bot user. - Once the bot user has been added, you can configure it by adding an avatar, - setting the display name, etc. You will also at this time have access to - your API token, which will be needed to configure this engine. + This engine requires a Slack app and a Slack Bot user. To create a + bot user, first go to the **Custom Integrations** page in your + Slack Workspace. Copy and paste the following URL, and log in with + account credentials with administrative privileges: + + ``https://api.slack.com/apps/new`` + + Next, click on the ``From scratch`` option from the ``Create an app`` popup. + Give your new app a unique name, eg. ``SaltSlackEngine``, select the workspace + where your app will be running, and click ``Create App``. + + Next, click on ``Socket Mode`` and then click on the toggle button for + ``Enable Socket Mode``. In the dialog give your Socket Mode Token a unique + name and then copy and save the app level token. This will be used + as the ``app_token`` parameter in the Slack engine configuration. + + Next, click on ``OAuth & Permissions`` and then under ``Bot Token Scope``, click + on ``Add an OAuth Scope``. Ensure the following scopes are included: + + - ``channels:history`` + - ``channels:read`` + - ``chat:write`` + - ``commands`` + - ``files:read`` + - ``files:write`` + - ``im:history`` + - ``mpim:history`` + - ``usergroups:read`` + - ``users:read`` + + Once all the scopes have been added, click the ``Install to Workspace`` button + under ``OAuth Tokens for Your Workspace``, then click ``Allow``. Copy and save + the ``Bot User OAuth Token``, this will be used as the ``bot_token`` parameter + in the Slack engine configuration. Finally, add this bot user to a channel by switching to the channel and using ``/invite @mybotuser``. Keep in mind that this engine will process @@ -74,6 +96,9 @@ .. versionchanged:: 2017.7.0 Access control group support added +.. versionchanged:: 3006.0 + Updated to use slack_bolt Python library. + This example uses a single group called ``default``. In addition, other groups are being loaded from pillar data. The group names do not have any significance, it is the users and commands defined within them that are used to @@ -83,7 +108,8 @@ engines: - slack: - token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx' + app_token: "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + bot_token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx' control: True fire_all: False groups_pillar_name: 'slack_engine:groups_pillar' @@ -121,7 +147,8 @@ engines: - slack: groups_pillar: slack_engine_pillar - token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx' + app_token: "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + bot_token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx' control: True fire_all: True tag: salt/engines/slack From 0ae55b1116f9f14f52161c9d0ac86979a67867b4 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Thu, 27 Oct 2022 09:40:13 -0700 Subject: [PATCH 4/5] Adding some additional tests for the slack engine. --- salt/engines/slack.py | 7 +- tests/pytests/unit/engines/test_slack.py | 251 ++++++++++++++++++++++- 2 files changed, 249 insertions(+), 9 deletions(-) diff --git a/salt/engines/slack.py b/salt/engines/slack.py index a079781f1f5d..fb69ab2417a4 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -233,6 +233,9 @@ def __init__(self, app_token, bot_token, trigger_string): # with the trigger string self.app.message(re.compile(trigger_pattern))(self.message_trigger) + def _run_until(self): + return True + def message_trigger(self, message): # Add the received message to the queue self.msg_queue.append(message) @@ -607,7 +610,7 @@ def just_data(m_data): self.handler ) ) # Boom! - while True: + while self._run_until(): while self.msg_queue: msg = self.msg_queue.popleft() try: @@ -816,7 +819,7 @@ def run_commands_from_slack_async( outstanding = {} # set of job_id that we need to check for - while True: + while self._run_until(): log.trace("Sleeping for interval of %s", interval) time.sleep(interval) # Drain the slack messages, up to 10 messages at a clip diff --git a/tests/pytests/unit/engines/test_slack.py b/tests/pytests/unit/engines/test_slack.py index 5fc486b9c910..b6a0df32266d 100644 --- a/tests/pytests/unit/engines/test_slack.py +++ b/tests/pytests/unit/engines/test_slack.py @@ -4,12 +4,12 @@ import pytest import salt.config -import salt.engines.slack as slack -from tests.support.mock import MagicMock, patch +import salt.engines.slack as slack_engine +from tests.support.mock import MagicMock, call, patch pytestmark = [ pytest.mark.skipif( - slack.HAS_SLACKBOLT is False, reason="The slack_bolt is not installed" + slack_engine.HAS_SLACKBOLT is False, reason="The slack_bolt is not installed" ) ] @@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs - self.client = None + self.client = MockSlackBoltAppClient() self.logger = None self.proxy = None @@ -36,9 +36,21 @@ def message(self, *args, **kwargs): return MagicMock(return_value=True) +class MockSlackBoltAppClient: + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + def chat_postMessage(self, *args, **kwargs): + return MagicMock(return_value=True) + + def files_upload(self, *args, **kwargs): + return MagicMock(return_value=True) + + @pytest.fixture def configure_loader_modules(): - return {slack: {}} + return {slack_engine: {}} @pytest.fixture @@ -48,7 +60,7 @@ def slack_client(): bot_token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx" trigger = "!" - with patch.dict(slack.__opts__, mock_opts): + with patch.dict(slack_engine.__opts__, mock_opts): with patch( "slack_bolt.App", MagicMock(autospec=True, return_value=MockSlackBoltApp()) ): @@ -56,7 +68,7 @@ def slack_client(): "slack_bolt.adapter.socket_mode.SocketModeHandler", MagicMock(autospec=True, return_value=MockSlackBoltSocketMode()), ): - slack_client = slack.SlackClient(app_token, bot_token, trigger) + slack_client = slack_engine.SlackClient(app_token, bot_token, trigger) yield slack_client @@ -123,3 +135,228 @@ def test_control_message_target(slack_client): ) assert target_commandline == _expected + + +def test_run_commands_from_slack_async(slack_client): + """ + Test slack engine: test_run_commands_from_slack_async + """ + + mock_job_status = { + "20221027001127600438": { + "data": {"minion": {"return": True, "retcode": 0, "success": True}}, + "function": "test.ping", + } + } + + message_generator = [ + { + "message_data": { + "client_msg_id": "c1d0c13d-5e78-431e-9921-4786a7d27543", + "type": "message", + "text": '!test.ping target="minion"', + "user": "U02QY11UJ", + "ts": "1666829486.542159", + "blocks": [ + { + "type": "rich_text", + "block_id": "2vdy", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": '!test.ping target="minion"', + } + ], + } + ], + } + ], + "team": "T02QY11UG", + "channel": "C02QY11UQ", + "event_ts": "1666829486.542159", + "channel_type": "channel", + }, + "channel": "C02QY11UQ", + "user": "U02QY11UJ", + "user_name": "garethgreenaway", + "cmdline": ["test.ping"], + "target": {"target": "minion", "tgt_type": "glob"}, + } + ] + + mock_files_upload_resp = { + "ok": True, + "file": { + "id": "F047YTDGJF9", + "created": 1666883749, + "timestamp": 1666883749, + "name": "salt-results-20221027081549173603.yaml", + "title": "salt-results-20221027081549173603", + "mimetype": "text/plain", + "filetype": "yaml", + "pretty_type": "YAML", + "user": "U0485K894PN", + "user_team": "T02QY11UG", + "editable": True, + "size": 18, + "mode": "snippet", + "is_external": False, + "external_type": "", + "is_public": True, + "public_url_shared": False, + "display_as_bot": False, + "username": "", + "url_private": "", + "url_private_download": "", + "permalink": "", + "permalink_public": "", + "edit_link": "", + "preview": "minion:\n True", + "preview_highlight": "", + "lines": 2, + "lines_more": 0, + "preview_is_truncated": False, + "comments_count": 0, + "is_starred": False, + "shares": { + "public": { + "C02QY11UQ": [ + { + "reply_users": [], + "reply_users_count": 0, + "reply_count": 0, + "ts": "1666883749.485979", + "channel_name": "general", + "team_id": "T02QY11UG", + "share_user_id": "U0485K894PN", + } + ] + } + }, + "channels": ["C02QY11UQ"], + "groups": [], + "ims": [], + "has_rich_preview": False, + "file_access": "visible", + }, + } + + patch_app_client_files_upload = patch.object( + MockSlackBoltAppClient, + "files_upload", + MagicMock(autospec=True, return_value=mock_files_upload_resp), + ) + patch_app_client_chat_postMessage = patch.object( + MockSlackBoltAppClient, + "chat_postMessage", + MagicMock(autospec=True, return_value=True), + ) + patch_slack_client_run_until = patch.object( + slack_client, "_run_until", MagicMock(autospec=True, side_effect=[True, False]) + ) + patch_slack_client_run_command_async = patch.object( + slack_client, + "run_command_async", + MagicMock(autospec=True, return_value="20221027001127600438"), + ) + patch_slack_client_get_jobs_from_runner = patch.object( + slack_client, + "get_jobs_from_runner", + MagicMock(autospec=True, return_value=mock_job_status), + ) + + upload_calls = call( + channels="C02QY11UQ", + content="minion:\n True", + filename="salt-results-20221027090136014442.yaml", + ) + + chat_postMessage_calls = [ + call( + channel="C02QY11UQ", + text="@garethgreenaway's job is submitted as salt jid 20221027001127600438", + ), + call( + channel="C02QY11UQ", + text="@garethgreenaway's job `['test.ping']` (id: 20221027001127600438) (target: {'target': 'minion', 'tgt_type': 'glob'}) returned", + ), + ] + + # + # test with control as True and fire_all as False + # + with patch_slack_client_run_until, patch_slack_client_run_command_async, patch_slack_client_get_jobs_from_runner, patch_app_client_files_upload as app_client_files_upload, patch_app_client_chat_postMessage as app_client_chat_postMessage: + slack_client.run_commands_from_slack_async( + message_generator=message_generator, + fire_all=False, + tag="salt/engines/slack", + control=True, + ) + app_client_files_upload.asser_has_calls(upload_calls) + app_client_chat_postMessage.asser_has_calls(chat_postMessage_calls) + + # + # test with control and fire_all as True + # + patch_slack_client_run_until = patch.object( + slack_client, "_run_until", MagicMock(autospec=True, side_effect=[True, False]) + ) + + mock_event_send = MagicMock(return_value=True) + patch_event_send = patch.dict( + slack_engine.__salt__, {"event.send": mock_event_send} + ) + + event_send_calls = [ + call( + "salt/engines/slack/message", + { + "message_data": { + "client_msg_id": "c1d0c13d-5e78-431e-9921-4786a7d27543", + "type": "message", + "text": '!test.ping target="minion"', + "user": "U02QY11UJ", + "ts": "1666829486.542159", + "blocks": [ + { + "type": "rich_text", + "block_id": "2vdy", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": '!test.ping target="minion"', + } + ], + } + ], + } + ], + "team": "T02QY11UG", + "channel": "C02QY11UQ", + "event_ts": "1666829486.542159", + "channel_type": "channel", + }, + "channel": "C02QY11UQ", + "user": "U02QY11UJ", + "user_name": "garethgreenaway", + "cmdline": ["test.ping"], + "target": {"target": "minion", "tgt_type": "glob"}, + }, + ) + ] + with patch_slack_client_run_until, patch_slack_client_run_command_async, patch_slack_client_get_jobs_from_runner, patch_event_send, patch_app_client_files_upload as app_client_files_upload, patch_app_client_chat_postMessage as app_client_chat_postMessage: + slack_client.run_commands_from_slack_async( + message_generator=message_generator, + fire_all=True, + tag="salt/engines/slack", + control=True, + ) + app_client_files_upload.asser_has_calls(upload_calls) + app_client_chat_postMessage.asser_has_calls(chat_postMessage_calls) + mock_event_send.asser_has_calls(event_send_calls) From 126ed0801352d84f2292b9e92088ccdb0a64159a Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 28 Oct 2022 12:31:37 -0700 Subject: [PATCH 5/5] Adding a missing block to the setup documentation. --- salt/engines/slack.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/engines/slack.py b/salt/engines/slack.py index fb69ab2417a4..4bd349a64b15 100644 --- a/salt/engines/slack.py +++ b/salt/engines/slack.py @@ -22,6 +22,10 @@ name and then copy and save the app level token. This will be used as the ``app_token`` parameter in the Slack engine configuration. + Next, click on ``Event Subscriptions`` and ensure that ``Enable Events`` is in + the on position. Then add the following bot events, ``message.channel`` + and ``message.im`` to the ``Subcribe to bot events`` list. + Next, click on ``OAuth & Permissions`` and then under ``Bot Token Scope``, click on ``Add an OAuth Scope``. Ensure the following scopes are included: