Restore the previous slack engine and deprecate it, rename replace the slack engine to slack_bolt until deprecation

This commit is contained in:
Gareth J. Greenaway 2022-12-05 11:31:29 -08:00 committed by Megan Wilhite
parent 21463a2f46
commit 4212c320e6
24 changed files with 1765 additions and 568 deletions

1
changelog/63095.added Normal file
View file

@ -0,0 +1 @@
Restore the previous slack engine and deprecate it, rename replace the slack engine to slack_bolt until deprecation

View file

@ -0,0 +1 @@
Deprecating the Salt Slack engine in favor of the Salt Slack Bolt Engine.

View file

@ -23,6 +23,7 @@ engine modules
redis_sentinel
script
slack
slack_bolt_engine
sqs_events
stalekey
test

View file

@ -0,0 +1,5 @@
salt.engines.slack_bolt_engine
==============================
.. automodule:: salt.engines.slack_bolt_engine
:members:

View file

@ -14,3 +14,4 @@ mercurial
hglib
redis-py-cluster
python-consul
slack_bolt

View file

@ -798,6 +798,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0

View file

@ -797,6 +797,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
sqlparse==0.4.2

View file

@ -856,6 +856,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==3.0.4
# via gitdb
sqlparse==0.4.2

View file

@ -820,6 +820,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0

View file

@ -822,6 +822,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
sqlparse==0.4.2

View file

@ -879,6 +879,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==3.0.4
# via gitdb
sqlparse==0.4.2

View file

@ -846,6 +846,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0

View file

@ -848,6 +848,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
sqlparse==0.4.2

View file

@ -900,6 +900,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==3.0.4
# via gitdb
sqlparse==0.4.2

View file

@ -838,6 +838,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0

View file

@ -839,6 +839,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
sqlparse==0.4.2

View file

@ -890,6 +890,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==3.0.4
# via gitdb
sqlparse==0.4.2

View file

@ -349,6 +349,8 @@ botocore==1.21.27
# boto3
# moto
# s3transfer
cached-property==1.5.2
# via pygit2
cachetools==4.2.2
# via
# google-auth
@ -839,6 +841,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
snowballstemmer==2.1.0

View file

@ -347,6 +347,8 @@ botocore==1.21.27
# boto3
# moto
# s3transfer
cached-property==1.5.2
# via pygit2
cachetools==4.2.2
# via
# google-auth
@ -840,6 +842,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==4.0.0
# via gitdb
sqlparse==0.4.2

View file

@ -895,6 +895,10 @@ six==1.16.0
# vcert
# virtualenv
# websocket-client
slack-bolt==1.15.5
# via -r requirements/static/ci/linux.in
slack-sdk==3.19.5
# via slack-bolt
smmap==3.0.4
# via gitdb
sqlparse==0.4.2

View file

@ -3,47 +3,21 @@ An engine that reads messages from Slack and can act on them
.. versionadded:: 2016.3.0
:depends: `slack_bolt <https://pypi.org/project/slack_bolt/>`_ Python module
:depends: `slackclient <https://pypi.org/project/slackclient/>`_ Python module
.. important::
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:
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://api.slack.com/apps/new``
``https://myworkspace.slack.com/apps/manage/custom-integrations``
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 ``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:
- ``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.
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.
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
@ -100,9 +74,6 @@ Configuration Examples
.. 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
@ -112,8 +83,7 @@ determine whether the Slack user has permission to run the desired command.
engines:
- slack:
app_token: "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
bot_token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx'
token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx'
control: True
fire_all: False
groups_pillar_name: 'slack_engine:groups_pillar'
@ -151,8 +121,7 @@ must be quoted, or else PyYAML will fail to load the configuration.
engines:
- slack:
groups_pillar: slack_engine_pillar
app_token: "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
bot_token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx'
token: 'xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx'
control: True
fire_all: True
tag: salt/engines/slack
@ -177,7 +146,6 @@ must be quoted, or else PyYAML will fail to load the configuration.
"""
import ast
import collections
import datetime
import itertools
import logging
@ -198,12 +166,11 @@ import salt.utils.slack
import salt.utils.yaml
try:
import slack_bolt
import slack_bolt.adapter.socket_mode
import slackclient
HAS_SLACKBOLT = True
HAS_SLACKCLIENT = True
except ImportError:
HAS_SLACKBOLT = False
HAS_SLACKCLIENT = False
log = logging.getLogger(__name__)
@ -211,38 +178,17 @@ __virtualname__ = "slack"
def __virtual__():
if not HAS_SLACKBOLT:
return (False, "The 'slack_bolt' Python module could not be loaded")
if not HAS_SLACKCLIENT:
return (False, "The 'slackclient' Python module could not be loaded")
return __virtualname__
class SlackClient:
def __init__(self, app_token, bot_token, trigger_string):
def __init__(self, token):
self.master_minion = salt.minion.MasterMinion(__opts__)
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_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 _run_until(self):
return True
def message_trigger(self, message):
# Add the received message to the queue
self.msg_queue.append(message)
self.sc = slackclient.SlackClient(token)
self.slack_connect = self.sc.rtm_connect()
def get_slack_users(self, token):
"""
@ -597,12 +543,13 @@ class SlackClient:
return data
for sleeps in (5, 10, 30, 60):
if self.handler:
if self.slack_connect:
break
else:
# see https://api.slack.com/docs/rate-limits
log.warning(
"Slack connection is invalid, sleeping %s",
"Slack connection is invalid. Server: %s, sleeping %s",
self.sc.server,
sleeps,
)
time.sleep(
@ -611,51 +558,51 @@ class SlackClient:
else:
raise UserWarning(
"Connection to slack is still invalid, giving up: {}".format(
self.handler
self.slack_connect
)
) # Boom!
while self._run_until():
while self.msg_queue:
msg = self.msg_queue.popleft()
while True:
msg = self.sc.rtm_read()
for m_data in msg:
try:
msg_text = self.message_text(msg)
msg_text = self.message_text(m_data)
except (ValueError, TypeError) as msg_err:
log.debug(
"Got an error from trying to get the message text %s", msg_err
)
yield {"message_data": msg} # Not a message type from the API?
yield {"message_data": m_data} # Not a message type from the API?
continue
# Find the channel object from the channel name
channel = msg["channel"]
data = just_data(msg)
channel = self.sc.server.channels.find(m_data["channel"])
data = just_data(m_data)
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?",
msg.get("user"),
m_data.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": msg}
yield {"message_data": m_data}
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:
ret = {
"message_data": msg,
"channel": msg["channel"],
yield {
"message_data": m_data,
"channel": m_data["channel"],
"user": data["user_id"],
"user_name": data["user_name"],
"cmdline": cmdline,
"target": target,
}
yield ret
continue
else:
channel.send_message(
@ -823,13 +770,12 @@ class SlackClient:
outstanding = {} # set of job_id that we need to check for
while self._run_until():
while True:
log.trace("Sleeping for interval of %s", interval)
time.sleep(interval)
# Drain the slack messages, up to 10 messages at a clip
count = 0
for msg in message_generator:
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.
@ -849,20 +795,18 @@ class SlackClient:
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
)
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
text_msg = "@{}'s job is submitted as salt jid {}".format(
channel.send_message(
"@{}'s job is submitted as salt jid {}".format(
msg["user_name"], jid
)
self.app.client.chat_postMessage(
channel=msg["channel"], text=text_msg
)
count += 1
start_time = time.time()
@ -881,7 +825,7 @@ class SlackClient:
log.debug("ret to send back is %s", result)
# formatting function?
this_job = outstanding[jid]
channel = this_job["channel"]
channel = self.sc.server.channels.find(this_job["channel"])
return_text = self.format_return_text(result, function)
return_prefix = (
"@{}'s job `{}` (id: {}) (target: {}) returned".format(
@ -891,19 +835,19 @@ class SlackClient:
this_job["target"],
)
)
self.app.client.chat_postMessage(
channel=channel, text=return_prefix
)
channel.send_message(return_prefix)
ts = time.time()
st = datetime.datetime.fromtimestamp(ts).strftime("%Y%m%d%H%M%S%f")
filename = "salt-results-{}.yaml".format(st)
resp = self.app.client.files_upload(
channels=channel,
r = self.sc.api_call(
"files.upload",
channels=channel.id,
filename=filename,
content=return_text,
)
# Handle unicode return
log.debug("Got back %s via the slack client", resp)
log.debug("Got back %s via the slack client", r)
resp = salt.utils.yaml.safe_load(salt.utils.json.dumps(r))
if "ok" in resp and resp["ok"] is False:
this_job["channel"].send_message(
"Error: {}".format(resp["error"])
@ -971,8 +915,7 @@ class SlackClient:
def start(
app_token,
bot_token,
token,
control=False,
trigger="!",
groups=None,
@ -984,17 +927,23 @@ def start(
Listen to slack events and forward them to salt, new version
"""
if (not bot_token) or (not bot_token.startswith("xoxb")):
salt.utils.versions.warn_until(
"Argon",
"This 'slack' engine will be deprecated and "
"will be replace by the slack_bolt engine. This new "
"engine will use the new Bolt library from Slack and requires "
"a Slack app and a Slack bot account.",
)
if (not token) or (not 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")
try:
client = SlackClient(
app_token=app_token, bot_token=bot_token, trigger_string=trigger
)
client = SlackClient(token=token)
message_generator = client.generate_triggered_messages(
bot_token, trigger, groups, groups_pillar_name
token, trigger, groups, groups_pillar_name
)
client.run_commands_from_slack_async(message_generator, fire_all, tag, control)
except Exception: # pylint: disable=broad-except

File diff suppressed because it is too large Load diff

View file

@ -4,109 +4,29 @@ unit tests for the slack engine
import pytest
import salt.config
import salt.engines.slack as slack_engine
from tests.support.mock import MagicMock, call, patch
import salt.engines.slack as slack
from tests.support.mock import MagicMock, patch
pytestmark = [
pytest.mark.skipif(
slack_engine.HAS_SLACKBOLT is False, reason="The slack_bolt is not installed"
slack.HAS_SLACKCLIENT is False, reason="The SlackClient is not installed"
)
]
class MockRunnerClient:
"""
Mock RunnerClient class
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def asynchronous(self, *args, **kwargs):
"""
Mock asynchronous method
"""
return True
class MockLocalClient:
"""
Mock RunnerClient class
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def __enter__(self, *args, **kwargs):
return self
def __exit__(self, *args, **kwargs):
pass
def cmd_async(self, *args, **kwargs):
"""
Mock cmd_async method
"""
return True
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 = MockSlackBoltAppClient()
self.logger = None
self.proxy = None
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_engine: {}}
return {slack: {}}
@pytest.fixture
def slack_client(minion_opts):
def slack_client():
mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
app_token = "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
bot_token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
trigger = "!"
token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
with patch.dict(slack_engine.__opts__, minion_opts):
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_engine.SlackClient(app_token, bot_token, 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
@ -173,341 +93,3 @@ 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)
def test_run_command_async(slack_client):
"""
Test slack engine: test_run_command_async
"""
msg = {
"message_data": {
"client_msg_id": "6c71d7f9-a44d-402f-8f9f-d1bb5b650853",
"type": "message",
"text": '!test.ping target="minion"',
"user": "U02QY11UJ",
"ts": "1667427929.764169",
"blocks": [
{
"type": "rich_text",
"block_id": "AjL",
"elements": [
{
"type": "rich_text_section",
"elements": [
{"type": "text", "text": '!test.ping target="minion"'}
],
}
],
}
],
"team": "T02QY11UG",
"channel": "C02QY11UQ",
"event_ts": "1667427929.764169",
"channel_type": "channel",
},
"channel": "C02QY11UQ",
"user": "U02QY11UJ",
"user_name": "garethgreenaway",
"cmdline": ["test.ping"],
"target": {"target": "minion", "tgt_type": "glob"},
}
local_client_mock = MagicMock(autospec=True, return_value=MockLocalClient())
patch_local_client = patch("salt.client.LocalClient", local_client_mock)
local_client_cmd_async_mock = MagicMock(
autospec=True, return_value={"jid": "20221027001127600438"}
)
patch_local_client_cmd_async = patch.object(
MockLocalClient, "cmd_async", local_client_cmd_async_mock
)
expected_calls = [call("minion", "test.ping", arg=[], kwarg={}, tgt_type="glob")]
with patch_local_client, patch_local_client_cmd_async as local_client_cmd_async:
ret = slack_client.run_command_async(msg)
local_client_cmd_async.assert_has_calls(expected_calls)
msg = {
"message_data": {
"client_msg_id": "35f4783f-8913-4687-8f04-21182bcacd5a",
"type": "message",
"text": "!test.arg arg1 arg2 arg3 key1=value1 key2=value2",
"user": "U02QY11UJ",
"ts": "1667429460.576889",
"blocks": [
{
"type": "rich_text",
"block_id": "EAzTy",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "!test.arg arg1 arg2 arg3 key1=value1 key2=value2",
}
],
}
],
}
],
"team": "T02QY11UG",
"channel": "C02QY11UQ",
"event_ts": "1667429460.576889",
"channel_type": "channel",
},
"channel": "C02QY11UQ",
"user": "U02QY11UJ",
"user_name": "garethgreenaway",
"cmdline": ["test.arg", "arg1", "arg2", "arg3", "key1=value1", "key2=value2"],
"target": {"target": "*", "tgt_type": "glob"},
}
runner_client_mock = MagicMock(autospec=True, return_value=MockRunnerClient())
patch_runner_client = patch("salt.runner.RunnerClient", runner_client_mock)
runner_client_asynchronous_mock = MagicMock(
autospec=True, return_value={"jid": "20221027001127600438"}
)
patch_runner_client_asynchronous = patch.object(
MockRunnerClient, "asynchronous", runner_client_asynchronous_mock
)
expected_calls = [
call(
"test.arg",
{
"arg": ["arg1", "arg2", "arg3"],
"kwarg": {"key1": "value1", "key2": "value2"},
},
)
]
with patch_runner_client, patch_runner_client_asynchronous as runner_client_asynchronous:
ret = slack_client.run_command_async(msg)
runner_client_asynchronous.assert_has_calls(expected_calls)

View file

@ -0,0 +1,516 @@
"""
unit tests for the slack engine
"""
import pytest
import salt.config
import salt.engines.slack_bolt_engine as slack_bolt_engine
from tests.support.mock import MagicMock, call, patch
pytestmark = [
pytest.mark.skipif(
slack_bolt_engine.HAS_SLACKBOLT is False,
reason="The slack_bolt is not installed",
)
]
class MockRunnerClient:
"""
Mock RunnerClient class
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def asynchronous(self, *args, **kwargs):
"""
Mock asynchronous method
"""
return True
class MockLocalClient:
"""
Mock RunnerClient class
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def __enter__(self, *args, **kwargs):
return self
def __exit__(self, *args, **kwargs):
pass
def cmd_async(self, *args, **kwargs):
"""
Mock cmd_async method
"""
return True
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 = MockSlackBoltAppClient()
self.logger = None
self.proxy = None
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_bolt_engine: {}}
@pytest.fixture
def slack_client(minion_opts):
mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
app_token = "xapp-x-xxxxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
bot_token = "xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx"
trigger = "!"
with patch.dict(slack_bolt_engine.__opts__, minion_opts):
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_bolt_engine.SlackClient(
app_token, bot_token, trigger
)
yield slack_client
def test_control_message_target(slack_client):
"""
Test slack engine: control_message_target
"""
trigger_string = "!"
loaded_groups = {
"default": {
"targets": {},
"commands": {"cmd.run", "test.ping"},
"default_target": {"tgt_type": "glob", "target": "*"},
"users": {"gareth"},
"aliases": {
"whoami": {"cmd": "cmd.run whoami"},
"list_pillar": {"cmd": "pillar.items"},
},
}
}
slack_user_name = "gareth"
# Check for correct cmdline
_expected = (True, {"tgt_type": "glob", "target": "*"}, ["cmd.run", "whoami"])
text = "!cmd.run whoami"
target_commandline = slack_client.control_message_target(
slack_user_name, text, loaded_groups, trigger_string
)
assert target_commandline == _expected
# Check aliases result in correct cmdline
text = "!whoami"
target_commandline = slack_client.control_message_target(
slack_user_name, text, loaded_groups, trigger_string
)
assert target_commandline == _expected
# Check pillar is overridden
_expected = (
True,
{"tgt_type": "glob", "target": "*"},
["pillar.items", 'pillar={"hello": "world"}'],
)
text = r"""!list_pillar pillar='{"hello": "world"}'"""
target_commandline = slack_client.control_message_target(
slack_user_name, text, loaded_groups, trigger_string
)
assert target_commandline == _expected
# Check target is overridden
_expected = (
True,
{"tgt_type": "glob", "target": "localhost"},
["cmd.run", "whoami"],
)
text = "!cmd.run whoami target='localhost'"
target_commandline = slack_client.control_message_target(
slack_user_name, text, loaded_groups, trigger_string
)
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_bolt_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)
def test_run_command_async(slack_client):
"""
Test slack engine: test_run_command_async
"""
msg = {
"message_data": {
"client_msg_id": "6c71d7f9-a44d-402f-8f9f-d1bb5b650853",
"type": "message",
"text": '!test.ping target="minion"',
"user": "U02QY11UJ",
"ts": "1667427929.764169",
"blocks": [
{
"type": "rich_text",
"block_id": "AjL",
"elements": [
{
"type": "rich_text_section",
"elements": [
{"type": "text", "text": '!test.ping target="minion"'}
],
}
],
}
],
"team": "T02QY11UG",
"channel": "C02QY11UQ",
"event_ts": "1667427929.764169",
"channel_type": "channel",
},
"channel": "C02QY11UQ",
"user": "U02QY11UJ",
"user_name": "garethgreenaway",
"cmdline": ["test.ping"],
"target": {"target": "minion", "tgt_type": "glob"},
}
local_client_mock = MagicMock(autospec=True, return_value=MockLocalClient())
patch_local_client = patch("salt.client.LocalClient", local_client_mock)
local_client_cmd_async_mock = MagicMock(
autospec=True, return_value={"jid": "20221027001127600438"}
)
patch_local_client_cmd_async = patch.object(
MockLocalClient, "cmd_async", local_client_cmd_async_mock
)
expected_calls = [call("minion", "test.ping", arg=[], kwarg={}, tgt_type="glob")]
with patch_local_client, patch_local_client_cmd_async as local_client_cmd_async:
ret = slack_client.run_command_async(msg)
local_client_cmd_async.assert_has_calls(expected_calls)
msg = {
"message_data": {
"client_msg_id": "35f4783f-8913-4687-8f04-21182bcacd5a",
"type": "message",
"text": "!test.arg arg1 arg2 arg3 key1=value1 key2=value2",
"user": "U02QY11UJ",
"ts": "1667429460.576889",
"blocks": [
{
"type": "rich_text",
"block_id": "EAzTy",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "!test.arg arg1 arg2 arg3 key1=value1 key2=value2",
}
],
}
],
}
],
"team": "T02QY11UG",
"channel": "C02QY11UQ",
"event_ts": "1667429460.576889",
"channel_type": "channel",
},
"channel": "C02QY11UQ",
"user": "U02QY11UJ",
"user_name": "garethgreenaway",
"cmdline": ["test.arg", "arg1", "arg2", "arg3", "key1=value1", "key2=value2"],
"target": {"target": "*", "tgt_type": "glob"},
}
runner_client_mock = MagicMock(autospec=True, return_value=MockRunnerClient())
patch_runner_client = patch("salt.runner.RunnerClient", runner_client_mock)
runner_client_asynchronous_mock = MagicMock(
autospec=True, return_value={"jid": "20221027001127600438"}
)
patch_runner_client_asynchronous = patch.object(
MockRunnerClient, "asynchronous", runner_client_asynchronous_mock
)
expected_calls = [
call(
"test.arg",
{
"arg": ["arg1", "arg2", "arg3"],
"kwarg": {"key1": "value1", "key2": "value2"},
},
)
]
with patch_runner_client, patch_runner_client_asynchronous as runner_client_asynchronous:
ret = slack_client.run_command_async(msg)
runner_client_asynchronous.assert_has_calls(expected_calls)