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