Skip to content

Commit

Permalink
Merge pull request #50879 from cdalvaro/returners/slack_webhook
Browse files Browse the repository at this point in the history
Add new slack returner based on incoming webhooks
  • Loading branch information
thatch45 authored Dec 26, 2018
2 parents 24bbc64 + f1dc9c2 commit 6bde57e
Showing 1 changed file with 338 additions and 0 deletions.
338 changes: 338 additions & 0 deletions salt/returners/slack_webhook_return.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
'''
Return salt data via slack
.. versionadded:: 2018.3.4
The following fields can be set in the minion conf file:
.. code-block:: yaml
slack_webhook.webhook (required, the webhook id. Just the part after: 'https://hooks.slack.com/services/')
slack_webhook.success_title (optional, short title for succeeded states. By default: '{id} | Succeeded')
slack_webhook.failure_title (optional, short title for failed states. By default: '{id} | Failed')
slack_webhook.author_icon (optional, a URL that with a small 16x16px image. Must be of type: GIF, JPEG, PNG, and BMP)
slack_webhook.show_tasks (optional, show identifiers for changed and failed tasks. By default: False)
Alternative configuration values can be used by prefacing the configuration.
Any values not found in the alternative configuration will be pulled from
the default location:
.. code-block:: yaml
slack_webhook.webhook
slack_webhook.success_title
slack_webhook.failure_title
slack_webhook.author_icon
slack_webhook.show_tasks
Slack settings may also be configured as:
.. code-block:: yaml
slack_webhook:
webhook: T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
success_title: [{id}] | Success
failure_title: [{id}] | Failure
author_icon: https://platform.slack-edge.com/img/default_application_icon.png
show_tasks: true
alternative.slack_webhook:
webhook: T00000000/C00000000/YYYYYYYYYYYYYYYYYYYYYYYY
show_tasks: false
To use the Slack returner, append '--return slack_webhook' to the salt command.
.. code-block:: bash
salt '*' test.ping --return slack_webhook
To use the alternative configuration, append '--return_config alternative' to the salt command.
.. code-block:: bash
salt '*' test.ping --return slack_webhook --return_config alternative
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import logging
import json

# pylint: disable=import-error,no-name-in-module,redefined-builtin
import salt.ext.six.moves.http_client
from salt.ext.six.moves.urllib.parse import urlencode as _urlencode
from salt.ext import six
from salt.ext.six.moves import map
from salt.ext.six.moves import range
# pylint: enable=import-error,no-name-in-module,redefined-builtin

# Import Salt Libs
import salt.returners
import salt.utils.http
import salt.utils.yaml

log = logging.getLogger(__name__)

__virtualname__ = 'slack_webhook'


def _get_options(ret=None):
'''
Get the slack_webhook options from salt.
:param ret: Salt return dictionary
:return: A dictionary with options
'''

defaults = {
'success_title': '{id} | Succeeded',
'failure_title': '{id} | Failed',
'author_icon': '',
'show_tasks': False
}

attrs = {
'webhook': 'webhook',
'success_title': 'success_title',
'failure_title': 'failure_title',
'author_icon': 'author_icon',
'show_tasks': 'show_tasks'
}

_options = salt.returners.get_returner_options(__virtualname__,
ret,
attrs,
__salt__=__salt__,
__opts__=__opts__,
defaults=defaults)
return _options


def __virtual__():
'''
Return virtual name of the module.
:return: The virtual name of the module.
'''
return __virtualname__


def _sprinkle(config_str):
'''
Sprinkle with grains of salt, that is
convert 'test {id} test {host} ' types of strings
:param config_str: The string to be sprinkled
:return: The string sprinkled
'''
parts = [x for sub in config_str.split('{') for x in sub.split('}')]
for i in range(1, len(parts), 2):
parts[i] = six.text_type(__grains__.get(parts[i], ''))
return ''.join(parts)


def _format_task(task):
'''
Return a dictionary with the task ready for slack fileds
:param task: The name of the task
:return: A dictionary ready to be inserted in Slack fields array
'''
return {'value': task, 'short': False}


def _generate_payload(author_icon, title, report):
'''
Prepare the payload for Slack
:param author_icon: The url for the thumbnail to be displayed
:param title: The title of the message
:param report: A dictionary with the report of the Salt function
:return: The payload ready for Slack
'''

title = _sprinkle(title)

unchanged = {
'color': 'good',
'title': 'Unchanged: {unchanged}'.format(unchanged=report['unchanged'].get('counter', None))
}

changed = {
'color': 'warning',
'title': 'Changed: {changed}'.format(changed=report['changed'].get('counter', None))
}

if len(report['changed'].get('tasks', [])) > 0:
changed['fields'] = list(
map(_format_task, report['changed'].get('tasks')))

failed = {
'color': 'danger',
'title': 'Failed: {failed}'.format(failed=report['failed'].get('counter', None))
}

if len(report['failed'].get('tasks', [])) > 0:
failed['fields'] = list(
map(_format_task, report['failed'].get('tasks')))

text = 'Function: {function}\n'.format(function=report.get('function'))
if len(report.get('arguments', [])) > 0:
text += 'Function Args: {arguments}\n'.format(
arguments=str(list(map(str, report.get('arguments')))))

text += 'JID: {jid}\n'.format(jid=report.get('jid'))
text += 'Total: {total}\n'.format(total=report.get('total'))
text += 'Duration: {duration:.2f} secs'.format(
duration=float(report.get('duration')))

payload = {
'attachments': [
{
'fallback': title,
'color': "#272727",
'author_name': _sprinkle('{id}'),
'author_link': _sprinkle('{localhost}'),
'author_icon': author_icon,
'title': 'Success: {success}'.format(success=str(report.get('success'))),
'text': text
},
unchanged,
changed,
failed
]
}

return payload


def _generate_report(ret, show_tasks):
'''
Generate a report of the Salt function
:param ret: The Salt return
:param show_tasks: Flag to show the name of the changed and failed states
:return: The report
'''

returns = ret.get('return')

sorted_data = sorted(
returns.items(),
key=lambda s: s[1].get('__run_num__', 0)
)

total = 0
failed = 0
changed = 0
duration = 0.0

changed_tasks = []
failed_tasks = []

# gather stats
for state, data in sorted_data:
# state: module, stateid, name, function
_, stateid, _, _ = state.split('_|-')
task = '{filename}.sls | {taskname}'.format(
filename=str(data.get('__sls__')), taskname=stateid)

if not data.get('result', True):
failed += 1
failed_tasks.append(task)

if data.get('changes', {}):
changed += 1
changed_tasks.append(task)

total += 1
try:
duration += float(data.get('duration', 0.0))
except ValueError:
pass

unchanged = total - failed - changed

log.debug('%s total: %s', __virtualname__, total)
log.debug('%s failed: %s', __virtualname__, failed)
log.debug('%s unchanged: %s', __virtualname__, unchanged)
log.debug('%s changed: %s', __virtualname__, changed)

report = {
'id': ret.get('id'),
'success': True if failed == 0 else False,
'total': total,
'function': ret.get('fun'),
'arguments': ret.get('fun_args', []),
'jid': ret.get('jid'),
'duration': duration / 1000,
'unchanged': {
'counter': unchanged
},
'changed': {
'counter': changed,
'tasks': changed_tasks if show_tasks else []
},
'failed': {
'counter': failed,
'tasks': failed_tasks if show_tasks else []
}
}

return report


def _post_message(webhook, author_icon, title, report):
'''
Send a message to a Slack room through a webhook
:param webhook: The url of the incoming webhook
:param author_icon: The thumbnail image to be displayed on the right side of the message
:param title: The title of the message
:param report: The report of the function state
:return: Boolean if message was sent successfully
'''

payload = _generate_payload(author_icon, title, report)

data = _urlencode({
'payload': json.dumps(payload, ensure_ascii=False)
})

webhook_url = 'https://hooks.slack.com/services/{webhook}'.format(webhook=webhook)
query_result = salt.utils.http.query(webhook_url, 'POST', data=data)

if query_result['body'] == 'ok' or query_result['status'] <= 201:
return True
else:
log.error('Slack incoming webhook message post result: %s', query_result)
return {
'res': False,
'message': query_result.get('body', query_result['status'])
}


def returner(ret):
'''
Send a slack message with the data through a webhook
:param ret: The Salt return
:return: The result of the post
'''

_options = _get_options(ret)

webhook = _options.get('webhook')
show_tasks = _options.get('show_tasks')
author_icon = _options.get('author_icon')

if not webhook:
log.error('%s.webhook not defined in salt config', __virtualname__)
return

report = _generate_report(ret, show_tasks)

if report.get('success'):
title = _options.get('success_title')
else:
title = _options.get('failure_title')

slack = _post_message(webhook, author_icon, title, report)

return slack

0 comments on commit 6bde57e

Please sign in to comment.