1# -*- coding: utf-8 -*-
2# (C) 2014-2015, Matt Martz <matt@sivel.net>
3# (C) 2017 Ansible Project
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6# Make coding more python3-ish
7from __future__ import (absolute_import, division, print_function)
8__metaclass__ = type
9
10DOCUMENTATION = '''
11    author: Unknown (!UNKNOWN)
12    name: slack
13    type: notification
14    requirements:
15      - whitelist in configuration
16      - prettytable (python library)
17    short_description: Sends play events to a Slack channel
18    description:
19        - This is an ansible callback plugin that sends status updates to a Slack channel during playbook execution.
20        - Before 2.4 only environment variables were available for configuring this plugin
21    options:
22      webhook_url:
23        required: True
24        description: Slack Webhook URL
25        env:
26          - name: SLACK_WEBHOOK_URL
27        ini:
28          - section: callback_slack
29            key: webhook_url
30      channel:
31        default: "#ansible"
32        description: Slack room to post in.
33        env:
34          - name: SLACK_CHANNEL
35        ini:
36          - section: callback_slack
37            key: channel
38      username:
39        description: Username to post as.
40        env:
41          - name: SLACK_USERNAME
42        default: ansible
43        ini:
44          - section: callback_slack
45            key: username
46      validate_certs:
47        description: validate the SSL certificate of the Slack server. (For HTTPS URLs)
48        env:
49          - name: SLACK_VALIDATE_CERTS
50        ini:
51          - section: callback_slack
52            key: validate_certs
53        default: True
54        type: bool
55'''
56
57import json
58import os
59import uuid
60
61from ansible import context
62from ansible.module_utils.common.text.converters import to_text
63from ansible.module_utils.urls import open_url
64from ansible.plugins.callback import CallbackBase
65
66try:
67    import prettytable
68    HAS_PRETTYTABLE = True
69except ImportError:
70    HAS_PRETTYTABLE = False
71
72
73class CallbackModule(CallbackBase):
74    """This is an ansible callback plugin that sends status
75    updates to a Slack channel during playbook execution.
76    """
77    CALLBACK_VERSION = 2.0
78    CALLBACK_TYPE = 'notification'
79    CALLBACK_NAME = 'community.general.slack'
80    CALLBACK_NEEDS_WHITELIST = True
81
82    def __init__(self, display=None):
83
84        super(CallbackModule, self).__init__(display=display)
85
86        if not HAS_PRETTYTABLE:
87            self.disabled = True
88            self._display.warning('The `prettytable` python module is not '
89                                  'installed. Disabling the Slack callback '
90                                  'plugin.')
91
92        self.playbook_name = None
93
94        # This is a 6 character identifier provided with each message
95        # This makes it easier to correlate messages when there are more
96        # than 1 simultaneous playbooks running
97        self.guid = uuid.uuid4().hex[:6]
98
99    def set_options(self, task_keys=None, var_options=None, direct=None):
100
101        super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
102
103        self.webhook_url = self.get_option('webhook_url')
104        self.channel = self.get_option('channel')
105        self.username = self.get_option('username')
106        self.show_invocation = (self._display.verbosity > 1)
107        self.validate_certs = self.get_option('validate_certs')
108
109        if self.webhook_url is None:
110            self.disabled = True
111            self._display.warning('Slack Webhook URL was not provided. The '
112                                  'Slack Webhook URL can be provided using '
113                                  'the `SLACK_WEBHOOK_URL` environment '
114                                  'variable.')
115
116    def send_msg(self, attachments):
117        headers = {
118            'Content-type': 'application/json',
119        }
120
121        payload = {
122            'channel': self.channel,
123            'username': self.username,
124            'attachments': attachments,
125            'parse': 'none',
126            'icon_url': ('https://cdn2.hubspot.net/hub/330046/'
127                         'file-449187601-png/ansible_badge.png'),
128        }
129
130        data = json.dumps(payload)
131        self._display.debug(data)
132        self._display.debug(self.webhook_url)
133        try:
134            response = open_url(self.webhook_url, data=data, validate_certs=self.validate_certs,
135                                headers=headers)
136            return response.read()
137        except Exception as e:
138            self._display.warning(u'Could not submit message to Slack: %s' %
139                                  to_text(e))
140
141    def v2_playbook_on_start(self, playbook):
142        self.playbook_name = os.path.basename(playbook._file_name)
143
144        title = [
145            '*Playbook initiated* (_%s_)' % self.guid
146        ]
147
148        invocation_items = []
149        if context.CLIARGS and self.show_invocation:
150            tags = context.CLIARGS['tags']
151            skip_tags = context.CLIARGS['skip_tags']
152            extra_vars = context.CLIARGS['extra_vars']
153            subset = context.CLIARGS['subset']
154            inventory = [os.path.abspath(i) for i in context.CLIARGS['inventory']]
155
156            invocation_items.append('Inventory:  %s' % ', '.join(inventory))
157            if tags and tags != ['all']:
158                invocation_items.append('Tags:       %s' % ', '.join(tags))
159            if skip_tags:
160                invocation_items.append('Skip Tags:  %s' % ', '.join(skip_tags))
161            if subset:
162                invocation_items.append('Limit:      %s' % subset)
163            if extra_vars:
164                invocation_items.append('Extra Vars: %s' %
165                                        ' '.join(extra_vars))
166
167            title.append('by *%s*' % context.CLIARGS['remote_user'])
168
169        title.append('\n\n*%s*' % self.playbook_name)
170        msg_items = [' '.join(title)]
171        if invocation_items:
172            msg_items.append('```\n%s\n```' % '\n'.join(invocation_items))
173
174        msg = '\n'.join(msg_items)
175
176        attachments = [{
177            'fallback': msg,
178            'fields': [
179                {
180                    'value': msg
181                }
182            ],
183            'color': 'warning',
184            'mrkdwn_in': ['text', 'fallback', 'fields'],
185        }]
186
187        self.send_msg(attachments=attachments)
188
189    def v2_playbook_on_play_start(self, play):
190        """Display Play start messages"""
191
192        name = play.name or 'Play name not specified (%s)' % play._uuid
193        msg = '*Starting play* (_%s_)\n\n*%s*' % (self.guid, name)
194        attachments = [
195            {
196                'fallback': msg,
197                'text': msg,
198                'color': 'warning',
199                'mrkdwn_in': ['text', 'fallback', 'fields'],
200            }
201        ]
202        self.send_msg(attachments=attachments)
203
204    def v2_playbook_on_stats(self, stats):
205        """Display info about playbook statistics"""
206
207        hosts = sorted(stats.processed.keys())
208
209        t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
210                                     'Failures', 'Rescued', 'Ignored'])
211
212        failures = False
213        unreachable = False
214
215        for h in hosts:
216            s = stats.summarize(h)
217
218            if s['failures'] > 0:
219                failures = True
220            if s['unreachable'] > 0:
221                unreachable = True
222
223            t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
224                                            'failures', 'rescued', 'ignored']])
225
226        attachments = []
227        msg_items = [
228            '*Playbook Complete* (_%s_)' % self.guid
229        ]
230        if failures or unreachable:
231            color = 'danger'
232            msg_items.append('\n*Failed!*')
233        else:
234            color = 'good'
235            msg_items.append('\n*Success!*')
236
237        msg_items.append('```\n%s\n```' % t)
238
239        msg = '\n'.join(msg_items)
240
241        attachments.append({
242            'fallback': msg,
243            'fields': [
244                {
245                    'value': msg
246                }
247            ],
248            'color': color,
249            'mrkdwn_in': ['text', 'fallback', 'fields']
250        })
251
252        self.send_msg(attachments=attachments)
253