1# -*- coding: utf-8 -*- 2# (c) 2015, Logentries.com, Jimmy Tang <jimmy.tang@logentries.com> 3# (c) 2017 Ansible Project 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8DOCUMENTATION = ''' 9 author: Unknown (!UNKNOWN) 10 name: logentries 11 type: notification 12 short_description: Sends events to Logentries 13 description: 14 - This callback plugin will generate JSON objects and send them to Logentries via TCP for auditing/debugging purposes. 15 - Before 2.4, if you wanted to use an ini configuration, the file must be placed in the same directory as this plugin and named logentries.ini 16 - In 2.4 and above you can just put it in the main Ansible configuration file. 17 requirements: 18 - whitelisting in configuration 19 - certifi (python library) 20 - flatdict (python library), if you want to use the 'flatten' option 21 options: 22 api: 23 description: URI to the Logentries API 24 env: 25 - name: LOGENTRIES_API 26 default: data.logentries.com 27 ini: 28 - section: callback_logentries 29 key: api 30 port: 31 description: HTTP port to use when connecting to the API 32 env: 33 - name: LOGENTRIES_PORT 34 default: 80 35 ini: 36 - section: callback_logentries 37 key: port 38 tls_port: 39 description: Port to use when connecting to the API when TLS is enabled 40 env: 41 - name: LOGENTRIES_TLS_PORT 42 default: 443 43 ini: 44 - section: callback_logentries 45 key: tls_port 46 token: 47 description: The logentries "TCP token" 48 env: 49 - name: LOGENTRIES_ANSIBLE_TOKEN 50 required: True 51 ini: 52 - section: callback_logentries 53 key: token 54 use_tls: 55 description: 56 - Toggle to decide whether to use TLS to encrypt the communications with the API server 57 env: 58 - name: LOGENTRIES_USE_TLS 59 default: False 60 type: boolean 61 ini: 62 - section: callback_logentries 63 key: use_tls 64 flatten: 65 description: flatten complex data structures into a single dictionary with complex keys 66 type: boolean 67 default: False 68 env: 69 - name: LOGENTRIES_FLATTEN 70 ini: 71 - section: callback_logentries 72 key: flatten 73''' 74 75EXAMPLES = ''' 76examples: > 77 To enable, add this to your ansible.cfg file in the defaults block 78 79 [defaults] 80 callback_whitelist = community.general.logentries 81 82 Either set the environment variables 83 export LOGENTRIES_API=data.logentries.com 84 export LOGENTRIES_PORT=10000 85 export LOGENTRIES_ANSIBLE_TOKEN=dd21fc88-f00a-43ff-b977-e3a4233c53af 86 87 Or in the main Ansible config file 88 [callback_logentries] 89 api = data.logentries.com 90 port = 10000 91 tls_port = 20000 92 use_tls = no 93 token = dd21fc88-f00a-43ff-b977-e3a4233c53af 94 flatten = False 95''' 96 97import os 98import socket 99import random 100import time 101import uuid 102 103try: 104 import certifi 105 HAS_CERTIFI = True 106except ImportError: 107 HAS_CERTIFI = False 108 109try: 110 import flatdict 111 HAS_FLATDICT = True 112except ImportError: 113 HAS_FLATDICT = False 114 115from ansible.module_utils.common.text.converters import to_bytes, to_text 116from ansible.plugins.callback import CallbackBase 117 118# Todo: 119# * Better formatting of output before sending out to logentries data/api nodes. 120 121 122class PlainTextSocketAppender(object): 123 def __init__(self, display, LE_API='data.logentries.com', LE_PORT=80, LE_TLS_PORT=443): 124 125 self.LE_API = LE_API 126 self.LE_PORT = LE_PORT 127 self.LE_TLS_PORT = LE_TLS_PORT 128 self.MIN_DELAY = 0.1 129 self.MAX_DELAY = 10 130 # Error message displayed when an incorrect Token has been detected 131 self.INVALID_TOKEN = "\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n" 132 # Unicode Line separator character \u2028 133 self.LINE_SEP = u'\u2028' 134 135 self._display = display 136 self._conn = None 137 138 def open_connection(self): 139 self._conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 140 self._conn.connect((self.LE_API, self.LE_PORT)) 141 142 def reopen_connection(self): 143 self.close_connection() 144 145 root_delay = self.MIN_DELAY 146 while True: 147 try: 148 self.open_connection() 149 return 150 except Exception as e: 151 self._display.vvvv(u"Unable to connect to Logentries: %s" % to_text(e)) 152 153 root_delay *= 2 154 if root_delay > self.MAX_DELAY: 155 root_delay = self.MAX_DELAY 156 157 wait_for = root_delay + random.uniform(0, root_delay) 158 159 try: 160 self._display.vvvv("sleeping %s before retry" % wait_for) 161 time.sleep(wait_for) 162 except KeyboardInterrupt: 163 raise 164 165 def close_connection(self): 166 if self._conn is not None: 167 self._conn.close() 168 169 def put(self, data): 170 # Replace newlines with Unicode line separator 171 # for multi-line events 172 data = to_text(data, errors='surrogate_or_strict') 173 multiline = data.replace(u'\n', self.LINE_SEP) 174 multiline += u"\n" 175 # Send data, reconnect if needed 176 while True: 177 try: 178 self._conn.send(to_bytes(multiline, errors='surrogate_or_strict')) 179 except socket.error: 180 self.reopen_connection() 181 continue 182 break 183 184 self.close_connection() 185 186 187try: 188 import ssl 189 HAS_SSL = True 190except ImportError: # for systems without TLS support. 191 SocketAppender = PlainTextSocketAppender 192 HAS_SSL = False 193else: 194 195 class TLSSocketAppender(PlainTextSocketAppender): 196 def open_connection(self): 197 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 198 sock = ssl.wrap_socket( 199 sock=sock, 200 keyfile=None, 201 certfile=None, 202 server_side=False, 203 cert_reqs=ssl.CERT_REQUIRED, 204 ssl_version=getattr( 205 ssl, 'PROTOCOL_TLSv1_2', ssl.PROTOCOL_TLSv1), 206 ca_certs=certifi.where(), 207 do_handshake_on_connect=True, 208 suppress_ragged_eofs=True, ) 209 sock.connect((self.LE_API, self.LE_TLS_PORT)) 210 self._conn = sock 211 212 SocketAppender = TLSSocketAppender 213 214 215class CallbackModule(CallbackBase): 216 CALLBACK_VERSION = 2.0 217 CALLBACK_TYPE = 'notification' 218 CALLBACK_NAME = 'community.general.logentries' 219 CALLBACK_NEEDS_WHITELIST = True 220 221 def __init__(self): 222 223 # TODO: allow for alternate posting methods (REST/UDP/agent/etc) 224 super(CallbackModule, self).__init__() 225 226 # verify dependencies 227 if not HAS_SSL: 228 self._display.warning("Unable to import ssl module. Will send over port 80.") 229 230 if not HAS_CERTIFI: 231 self.disabled = True 232 self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.') 233 234 self.le_jobid = str(uuid.uuid4()) 235 236 # FIXME: make configurable, move to options 237 self.timeout = 10 238 239 def set_options(self, task_keys=None, var_options=None, direct=None): 240 241 super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) 242 243 # get options 244 try: 245 self.api_url = self.get_option('api') 246 self.api_port = self.get_option('port') 247 self.api_tls_port = self.get_option('tls_port') 248 self.use_tls = self.get_option('use_tls') 249 self.flatten = self.get_option('flatten') 250 except KeyError as e: 251 self._display.warning(u"Missing option for Logentries callback plugin: %s" % to_text(e)) 252 self.disabled = True 253 254 try: 255 self.token = self.get_option('token') 256 except KeyError as e: 257 self._display.warning('Logentries token was not provided, this is required for this callback to operate, disabling') 258 self.disabled = True 259 260 if self.flatten and not HAS_FLATDICT: 261 self.disabled = True 262 self._display.warning('You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin.') 263 264 self._initialize_connections() 265 266 def _initialize_connections(self): 267 268 if not self.disabled: 269 if self.use_tls: 270 self._display.vvvv("Connecting to %s:%s with TLS" % (self.api_url, self.api_tls_port)) 271 self._appender = TLSSocketAppender(display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port) 272 else: 273 self._display.vvvv("Connecting to %s:%s" % (self.api_url, self.api_port)) 274 self._appender = PlainTextSocketAppender(display=self._display, LE_API=self.api_url, LE_PORT=self.api_port) 275 self._appender.reopen_connection() 276 277 def emit_formatted(self, record): 278 if self.flatten: 279 results = flatdict.FlatDict(record) 280 self.emit(self._dump_results(results)) 281 else: 282 self.emit(self._dump_results(record)) 283 284 def emit(self, record): 285 msg = record.rstrip('\n') 286 msg = "{0} {1}".format(self.token, msg) 287 self._appender.put(msg) 288 self._display.vvvv("Sent event to logentries") 289 290 def _set_info(self, host, res): 291 return {'le_jobid': self.le_jobid, 'hostname': host, 'results': res} 292 293 def runner_on_ok(self, host, res): 294 results = self._set_info(host, res) 295 results['status'] = 'OK' 296 self.emit_formatted(results) 297 298 def runner_on_failed(self, host, res, ignore_errors=False): 299 results = self._set_info(host, res) 300 results['status'] = 'FAILED' 301 self.emit_formatted(results) 302 303 def runner_on_skipped(self, host, item=None): 304 results = self._set_info(host, item) 305 del results['results'] 306 results['status'] = 'SKIPPED' 307 self.emit_formatted(results) 308 309 def runner_on_unreachable(self, host, res): 310 results = self._set_info(host, res) 311 results['status'] = 'UNREACHABLE' 312 self.emit_formatted(results) 313 314 def runner_on_async_failed(self, host, res, jid): 315 results = self._set_info(host, res) 316 results['jid'] = jid 317 results['status'] = 'ASYNC_FAILED' 318 self.emit_formatted(results) 319 320 def v2_playbook_on_play_start(self, play): 321 results = {} 322 results['le_jobid'] = self.le_jobid 323 results['started_by'] = os.getlogin() 324 if play.name: 325 results['play'] = play.name 326 results['hosts'] = play.hosts 327 self.emit_formatted(results) 328 329 def playbook_on_stats(self, stats): 330 """ close connection """ 331 self._appender.close_connection() 332