Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 slack callback plugin #13408

Merged
merged 1 commit into from
Feb 18, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
230 changes: 230 additions & 0 deletions lib/ansible/plugins/callback/slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# (C) 2014-2015, Matt Martz <matt@sivel.net>

# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import json
import os
import uuid

from __main__ import cli

from ansible.constants import mk_boolean
from ansible.module_utils.urls import open_url
from ansible.plugins.callback import CallbackBase

try:
import prettytable
HAS_PRETTYTABLE = True
except ImportError:
HAS_PRETTYTABLE = False


class CallbackModule(CallbackBase):
"""This is an ansible callback plugin that sends status
updates to a Slack channel during playbook execution.

This plugin makes use of the following environment variables:
SLACK_WEBHOOK_URL (required): Slack Webhook URL
SLACK_CHANNEL (optional): Slack room to post in. Default: #ansible
SLACK_USERNAME (optional): Username to post as. Default: ansible
SLACK_INVOCATION (optional): Show command line invocation
details. Default: False

Requires:
prettytable

"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'slack'
CALLBACK_NEEDS_WHITELIST = True

def __init__(self, display=None):

self.disabled = False

if cli:
self._options = cli.options
else:
self._options = None


super(CallbackModule, self).__init__(display=display)

if not HAS_PRETTYTABLE:
self.disabled = True
self._display.warning('The `prettytable` python module is not '
'installed. Disabling the Slack callback '
'plugin.')

self.webhook_url = os.getenv('SLACK_WEBHOOK_URL')
self.channel = os.getenv('SLACK_CHANNEL', '#ansible')
self.username = os.getenv('SLACK_USERNAME', 'ansible')
self.show_invocation = mk_boolean(
os.getenv('SLACK_INVOCATION', self._display.verbosity > 1)
)

if self.webhook_url is None:
self.disabled = True
self._display.warning('Slack Webhook URL was not provided. The '
'Slack Webhook URL can be provided using '
'the `SLACK_WEBHOOK_URL` environment '
'variable.')

self.playbook_name = None

# This is a 6 character identifier provided with each message
# This makes it easier to correlate messages when there are more
# than 1 simultaneous playbooks running
self.guid = uuid.uuid4().hex[:6]

def send_msg(self, attachments):
payload = {
'channel': self.channel,
'username': self.username,
'attachments': attachments,
'parse': 'none',
'icon_url': ('http://cdn2.hubspot.net/hub/330046/'
'file-449187601-png/ansible_badge.png'),
}

data = json.dumps(payload)
self._display.debug(data)
self._display.debug(self.webhook_url)
try:
response = open_url(self.webhook_url, data=data)
return response.read()
except Exception as e:
self._display.warning('Could not submit message to Slack: %s' %
str(e))

def v2_playbook_on_start(self, playbook):
self.playbook_name = os.path.basename(playbook._file_name)

title = [
'*Playbook initiated* (_%s_)' % self.guid
]
invocation_items = []
if self._options and self.show_invocation:
tags = self._options.tags
skip_tags = self._options.skip_tags
extra_vars = self._options.extra_vars
subset = self._options.subset
inventory = os.path.basename(
os.path.realpath(self._options.inventory)
)

invocation_items.append('Inventory: %s' % inventory)
if tags and tags != 'all':
invocation_items.append('Tags: %s' % tags)
if skip_tags:
invocation_items.append('Skip Tags: %s' % skip_tags)
if subset:
invocation_items.append('Limit: %s' % subset)
if extra_vars:
invocation_items.append('Extra Vars: %s' %
' '.join(extra_vars))

title.append('by *%s*' % self._options.remote_user)

title.append('\n\n*%s*' % self.playbook_name)
msg_items = [' '.join(title)]
if invocation_items:
msg_items.append('```\n%s\n```' % '\n'.join(invocation_items))

msg = '\n'.join(msg_items)

attachments = [{
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
}]

self.send_msg(attachments=attachments)

def v2_playbook_on_play_start(self, play):
"""Display Play start messages"""

name = play.name or 'Play name not specified (%s)' % play._uuid
msg = '*Starting play* (_%s_)\n\n*%s*' % (self.guid, name)
attachments = [
{
'fallback': msg,
'text': msg,
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
}
]
self.send_msg(attachments=attachments)

def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics"""

hosts = sorted(stats.processed.keys())

t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
'Failures'])

failures = False
unreachable = False

for h in hosts:
s = stats.summarize(h)

if s['failures'] > 0:
failures = True
if s['unreachable'] > 0:
unreachable = True

t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
'failures']])

attachments = []
msg_items = [
'*Playbook Complete* (_%s_)' % self.guid
]
if failures or unreachable:
color = 'danger'
msg_items.append('\n*Failed!*')
else:
color = 'good'
msg_items.append('\n*Success!*')

msg_items.append('```\n%s\n```' % t)

msg = '\n'.join(msg_items)

attachments.append({
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': color,
'mrkdwn_in': ['text', 'fallback', 'fields']
})

self.send_msg(attachments=attachments)