mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
cc343a4376
* Using docstrings conflicts with the standard use of docstrings * PYTHON_OPTIMIZE=2 will omit docstrings. Using docstrings makes future changes to the plugin and module code subject to the requirement that we ensure it won't be run with optimization.
347 lines
11 KiB
Python
347 lines
11 KiB
Python
# (c) 2015, Logentries.com, Jimmy Tang <jimmy.tang@logentries.com>
|
|
# (c) 2017 Ansible Project
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
DOCUMENTATION = '''
|
|
callback: logentries
|
|
type: notification
|
|
short_description: Sends events to Logentries
|
|
description:
|
|
- This callback plugin will generate JSON objects and send them to Logentries via TCP for auditing/debugging purposes.
|
|
- 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
|
|
- In 2.4 and above you can just put it in the main Ansible configuration file.
|
|
version_added: "2.0"
|
|
requirements:
|
|
- whitelisting in configuration
|
|
- certifi (python library)
|
|
- flatdict (pytnon library), if you want to use the 'flatten' option
|
|
options:
|
|
api:
|
|
description: URI to the Logentries API
|
|
env:
|
|
- name: LOGENTRIES_API
|
|
default: data.logentries.com
|
|
ini:
|
|
- section: callback_logentries
|
|
key: api
|
|
port:
|
|
description: Http port to use when connecting to the API
|
|
env:
|
|
- name: LOGENTRIES_PORT
|
|
default: 80
|
|
ini:
|
|
- section: callback_logentries
|
|
key: port
|
|
tls_port:
|
|
description: Port to use when connecting to the API when TLS is enabled
|
|
env:
|
|
- name: LOGENTRIES_TLS_PORT
|
|
default: 443
|
|
ini:
|
|
- section: callback_logentries
|
|
key: tls_port
|
|
token:
|
|
description: The logentries "TCP token"
|
|
env:
|
|
- name: LOGENTRIES_ANSIBLE_TOKEN
|
|
required: True
|
|
ini:
|
|
- section: callback_logentries
|
|
key: token
|
|
use_tls:
|
|
description:
|
|
- Toggle to decidewhether to use TLS to encrypt the communications with the API server
|
|
env:
|
|
- name: LOGENTRIES_USE_TLS
|
|
default: False
|
|
type: boolean
|
|
ini:
|
|
- section: callback_logentries
|
|
key: use_tls
|
|
flatten:
|
|
description: flatten complex data structures into a single dictionary with complex keys
|
|
type: boolean
|
|
default: False
|
|
env:
|
|
- name: LOGENTRIES_FLATTEN
|
|
ini:
|
|
- section: callback_logentries
|
|
key: flatten
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
examples: >
|
|
To enable, add this to your ansible.cfg file in the defaults block
|
|
|
|
[defaults]
|
|
callback_whitelist = logentries
|
|
|
|
Either set the environment variables
|
|
export LOGENTRIES_API=data.logentries.com
|
|
export LOGENTRIES_PORT=10000
|
|
export LOGENTRIES_ANSIBLE_TOKEN=dd21fc88-f00a-43ff-b977-e3a4233c53af
|
|
|
|
Or in the main Ansible config file
|
|
[callback_logentries]
|
|
api = data.logentries.com
|
|
port = 10000
|
|
tls_port = 20000
|
|
use_tls = no
|
|
token = dd21fc88-f00a-43ff-b977-e3a4233c53af
|
|
flatten = False
|
|
'''
|
|
|
|
import os
|
|
import socket
|
|
import random
|
|
import time
|
|
import uuid
|
|
|
|
try:
|
|
import certifi
|
|
HAS_CERTIFI = True
|
|
except ImportError:
|
|
HAS_CERTIFI = False
|
|
|
|
try:
|
|
import flatdict
|
|
HAS_FLATDICT = True
|
|
except ImportError:
|
|
HAS_FLATDICT = False
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.module_utils._text import to_bytes, to_text, to_native
|
|
from ansible.plugins.callback import CallbackBase
|
|
|
|
# Todo:
|
|
# * Better formatting of output before sending out to logentries data/api nodes.
|
|
|
|
|
|
class PlainTextSocketAppender(object):
|
|
def __init__(self, display, LE_API='data.logentries.com', LE_PORT=80, LE_TLS_PORT=443):
|
|
|
|
self.LE_API = LE_API
|
|
self.LE_PORT = LE_PORT
|
|
self.LE_TLS_PORT = LE_TLS_PORT
|
|
self.MIN_DELAY = 0.1
|
|
self.MAX_DELAY = 10
|
|
# Error message displayed when an incorrect Token has been detected
|
|
self.INVALID_TOKEN = ("\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n")
|
|
# Unicode Line separator character \u2028
|
|
self.LINE_SEP = u'\u2028'
|
|
|
|
self._display = display
|
|
self._conn = None
|
|
|
|
def open_connection(self):
|
|
self._conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self._conn.connect((self.LE_API, self.LE_PORT))
|
|
|
|
def reopen_connection(self):
|
|
self.close_connection()
|
|
|
|
root_delay = self.MIN_DELAY
|
|
while True:
|
|
try:
|
|
self.open_connection()
|
|
return
|
|
except Exception as e:
|
|
self._display.vvvv("Unable to connect to Logentries: %s" % str(e))
|
|
|
|
root_delay *= 2
|
|
if (root_delay > self.MAX_DELAY):
|
|
root_delay = self.MAX_DELAY
|
|
|
|
wait_for = root_delay + random.uniform(0, root_delay)
|
|
|
|
try:
|
|
self._display.vvvv("sleeping %s before retry" % wait_for)
|
|
time.sleep(wait_for)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
|
|
def close_connection(self):
|
|
if self._conn is not None:
|
|
self._conn.close()
|
|
|
|
def put(self, data):
|
|
# Replace newlines with Unicode line separator
|
|
# for multi-line events
|
|
data = to_text(data, errors='surrogate_or_strict')
|
|
multiline = data.replace(u'\n', self.LINE_SEP)
|
|
multiline += u"\n"
|
|
# Send data, reconnect if needed
|
|
while True:
|
|
try:
|
|
self._conn.send(to_bytes(multiline, errors='surrogate_or_strict'))
|
|
except socket.error:
|
|
self.reopen_connection()
|
|
continue
|
|
break
|
|
|
|
self.close_connection()
|
|
|
|
|
|
try:
|
|
import ssl
|
|
HAS_SSL = True
|
|
except ImportError: # for systems without TLS support.
|
|
SocketAppender = PlainTextSocketAppender
|
|
HAS_SSL = False
|
|
else:
|
|
|
|
class TLSSocketAppender(PlainTextSocketAppender):
|
|
def open_connection(self):
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock = ssl.wrap_socket(
|
|
sock=sock,
|
|
keyfile=None,
|
|
certfile=None,
|
|
server_side=False,
|
|
cert_reqs=ssl.CERT_REQUIRED,
|
|
ssl_version=getattr(
|
|
ssl, 'PROTOCOL_TLSv1_2', ssl.PROTOCOL_TLSv1),
|
|
ca_certs=certifi.where(),
|
|
do_handshake_on_connect=True,
|
|
suppress_ragged_eofs=True, )
|
|
sock.connect((self.LE_API, self.LE_TLS_PORT))
|
|
self._conn = sock
|
|
|
|
SocketAppender = TLSSocketAppender
|
|
|
|
|
|
class CallbackModule(CallbackBase):
|
|
CALLBACK_VERSION = 2.0
|
|
CALLBACK_TYPE = 'notification'
|
|
CALLBACK_NAME = 'logentries'
|
|
CALLBACK_NEEDS_WHITELIST = True
|
|
|
|
def __init__(self):
|
|
|
|
# TODO: allow for alternate posting methods (REST/UDP/agent/etc)
|
|
super(CallbackModule, self).__init__()
|
|
|
|
# verify dependencies
|
|
if not HAS_SSL:
|
|
self._display.warning("Unable to import ssl module. Will send over port 80.")
|
|
|
|
if not HAS_CERTIFI:
|
|
self.disabled = True
|
|
self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.')
|
|
|
|
self.le_jobid = str(uuid.uuid4())
|
|
|
|
# FIXME: remove when done testing
|
|
# initialize configurable
|
|
self.api_url = 'data.logentries.com'
|
|
self.api_port = 80
|
|
self.api_tls_port = 443
|
|
self.use_tls = False
|
|
self.flatten = False
|
|
self.token = None
|
|
|
|
# FIXME: make configurable, move to options
|
|
self.timeout = 10
|
|
|
|
# FIXME: remove testing
|
|
# self.set_options({'api': 'data.logentries.com', 'port': 80,
|
|
# 'tls_port': 10000, 'use_tls': True, 'flatten': False, 'token': 'ae693734-4c5b-4a44-8814-1d2feb5c8241'})
|
|
|
|
def set_option(self, name, value):
|
|
raise AnsibleError("The Logentries callabck plugin does not suport setting individual options.")
|
|
|
|
def set_options(self, options):
|
|
|
|
super(CallbackModule, self).set_options(options)
|
|
|
|
# get options
|
|
try:
|
|
self.api_url = self._plugin_options['api']
|
|
self.api_port = self._plugin_options['port']
|
|
self.api_tls_port = self._plugin_options['tls_port']
|
|
self.use_tls = self._plugin_options['use_tls']
|
|
self.flatten = self._plugin_options['flatten']
|
|
except KeyError as e:
|
|
self._display.warning("Missing option for Logentries callback plugin: %s" % to_native(e))
|
|
self.disabled = True
|
|
|
|
try:
|
|
self.token = self._plugin_options['token']
|
|
except KeyError as e:
|
|
self._display.warning('Logentries token was not provided, this is required for this callback to operate, disabling')
|
|
self.disabled = True
|
|
|
|
if self.flatten and not HAS_FLATDICT:
|
|
self.disabled = True
|
|
self._display.warning('You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin.')
|
|
|
|
self._initialize_connections()
|
|
|
|
def _initialize_connections(self):
|
|
|
|
if not self.disabled:
|
|
if self.use_tls:
|
|
self._display.vvvv("Connecting to %s:%s with TLS" % (self.api_url, self.api_tls_port))
|
|
self._appender = TLSSocketAppender(display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port)
|
|
else:
|
|
self._display.vvvv("Connecting to %s:%s" % (self.api_url, self.api_port))
|
|
self._appender = PlainTextSocketAppender(display=self._display, LE_API=self.api_url, LE_PORT=self.api_port)
|
|
self._appender.reopen_connection()
|
|
|
|
def emit_formatted(self, record):
|
|
if self.flatten:
|
|
results = flatdict.FlatDict(record)
|
|
self.emit(self._dump_results(results))
|
|
else:
|
|
self.emit(self._dump_results(record))
|
|
|
|
def emit(self, record):
|
|
msg = record.rstrip('\n')
|
|
msg = "{} {}".format(self.token, msg)
|
|
self._appender.put(msg)
|
|
self._display.vvvv("Sent event to logentries")
|
|
|
|
def _set_info(self, host, res):
|
|
return {'le_jobid': self.le_jobid, 'hostname': host, 'results': res}
|
|
|
|
def runner_on_ok(self, host, res):
|
|
results = self._set_info(host, res)
|
|
results['status'] = 'OK'
|
|
self.emit_formatted(results)
|
|
|
|
def runner_on_failed(self, host, res, ignore_errors=False):
|
|
results = self._set_info(host, res)
|
|
results['status'] = 'FAILED'
|
|
self.emit_formatted(results)
|
|
|
|
def runner_on_skipped(self, host, item=None):
|
|
results = self._set_info(host, item)
|
|
del results['results']
|
|
results['status'] = 'SKIPPED'
|
|
self.emit_formatted(results)
|
|
|
|
def runner_on_unreachable(self, host, res):
|
|
results = self._set_info(host, res)
|
|
results['status'] = 'UNREACHABLE'
|
|
self.emit_formatted(results)
|
|
|
|
def runner_on_async_failed(self, host, res, jid):
|
|
results = self._set_info(host, res)
|
|
results['jid'] = jid
|
|
results['status'] = 'ASYNC_FAILED'
|
|
self.emit_formatted(results)
|
|
|
|
def v2_playbook_on_play_start(self, play):
|
|
results = {}
|
|
results['le_jobid'] = self.le_jobid
|
|
results['started_by'] = os.getlogin()
|
|
if play.name:
|
|
results['play'] = play.name
|
|
results['hosts'] = play.hosts
|
|
self.emit_formatted(results)
|
|
|
|
def playbook_on_stats(self, stats):
|
|
""" close connection """
|
|
self._appender.close_connection()
|