1# -*- coding: utf-8 -*- 2# (c) 2018, Samir Musali <samir.musali@logdna.com> 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8DOCUMENTATION = ''' 9 author: Unknown (!UNKNOWN) 10 name: logdna 11 type: aggregate 12 short_description: Sends playbook logs to LogDNA 13 description: 14 - This callback will report logs from playbook actions, tasks, and events to LogDNA (https://app.logdna.com) 15 requirements: 16 - LogDNA Python Library (https://github.com/logdna/python) 17 - whitelisting in configuration 18 options: 19 conf_key: 20 required: True 21 description: LogDNA Ingestion Key 22 type: string 23 env: 24 - name: LOGDNA_INGESTION_KEY 25 ini: 26 - section: callback_logdna 27 key: conf_key 28 plugin_ignore_errors: 29 required: False 30 description: Whether to ignore errors on failing or not 31 type: boolean 32 env: 33 - name: ANSIBLE_IGNORE_ERRORS 34 ini: 35 - section: callback_logdna 36 key: plugin_ignore_errors 37 default: False 38 conf_hostname: 39 required: False 40 description: Alternative Host Name; the current host name by default 41 type: string 42 env: 43 - name: LOGDNA_HOSTNAME 44 ini: 45 - section: callback_logdna 46 key: conf_hostname 47 conf_tags: 48 required: False 49 description: Tags 50 type: string 51 env: 52 - name: LOGDNA_TAGS 53 ini: 54 - section: callback_logdna 55 key: conf_tags 56 default: ansible 57''' 58 59import logging 60import json 61import socket 62from uuid import getnode 63from ansible.plugins.callback import CallbackBase 64from ansible.parsing.ajson import AnsibleJSONEncoder 65 66try: 67 from logdna import LogDNAHandler 68 HAS_LOGDNA = True 69except ImportError: 70 HAS_LOGDNA = False 71 72 73# Getting MAC Address of system: 74def get_mac(): 75 mac = "%012x" % getnode() 76 return ":".join(map(lambda index: mac[index:index + 2], range(int(len(mac) / 2)))) 77 78 79# Getting hostname of system: 80def get_hostname(): 81 return str(socket.gethostname()).split('.local', 1)[0] 82 83 84# Getting IP of system: 85def get_ip(): 86 try: 87 return socket.gethostbyname(get_hostname()) 88 except Exception: 89 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 90 try: 91 s.connect(('10.255.255.255', 1)) 92 IP = s.getsockname()[0] 93 except Exception: 94 IP = '127.0.0.1' 95 finally: 96 s.close() 97 return IP 98 99 100# Is it JSON? 101def isJSONable(obj): 102 try: 103 json.dumps(obj, sort_keys=True, cls=AnsibleJSONEncoder) 104 return True 105 except Exception: 106 return False 107 108 109# LogDNA Callback Module: 110class CallbackModule(CallbackBase): 111 112 CALLBACK_VERSION = 0.1 113 CALLBACK_TYPE = 'aggregate' 114 CALLBACK_NAME = 'community.general.logdna' 115 CALLBACK_NEEDS_WHITELIST = True 116 117 def __init__(self, display=None): 118 super(CallbackModule, self).__init__(display=display) 119 120 self.disabled = True 121 self.playbook_name = None 122 self.playbook = None 123 self.conf_key = None 124 self.plugin_ignore_errors = None 125 self.conf_hostname = None 126 self.conf_tags = None 127 128 def set_options(self, task_keys=None, var_options=None, direct=None): 129 super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) 130 131 self.conf_key = self.get_option('conf_key') 132 self.plugin_ignore_errors = self.get_option('plugin_ignore_errors') 133 self.conf_hostname = self.get_option('conf_hostname') 134 self.conf_tags = self.get_option('conf_tags') 135 self.mac = get_mac() 136 self.ip = get_ip() 137 138 if self.conf_hostname is None: 139 self.conf_hostname = get_hostname() 140 141 self.conf_tags = self.conf_tags.split(',') 142 143 if HAS_LOGDNA: 144 self.log = logging.getLogger('logdna') 145 self.log.setLevel(logging.INFO) 146 self.options = {'hostname': self.conf_hostname, 'mac': self.mac, 'index_meta': True} 147 self.log.addHandler(LogDNAHandler(self.conf_key, self.options)) 148 self.disabled = False 149 else: 150 self.disabled = True 151 self._display.warning('WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`') 152 153 def metaIndexing(self, meta): 154 invalidKeys = [] 155 ninvalidKeys = 0 156 for key, value in meta.items(): 157 if not isJSONable(value): 158 invalidKeys.append(key) 159 ninvalidKeys += 1 160 if ninvalidKeys > 0: 161 for key in invalidKeys: 162 del meta[key] 163 meta['__errors'] = 'These keys have been sanitized: ' + ', '.join(invalidKeys) 164 return meta 165 166 def sanitizeJSON(self, data): 167 try: 168 return json.loads(json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)) 169 except Exception: 170 return {'warnings': ['JSON Formatting Issue', json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]} 171 172 def flush(self, log, options): 173 if HAS_LOGDNA: 174 self.log.info(json.dumps(log), options) 175 176 def sendLog(self, host, category, logdata): 177 options = {'app': 'ansible', 'meta': {'playbook': self.playbook_name, 'host': host, 'category': category}} 178 logdata['info'].pop('invocation', None) 179 warnings = logdata['info'].pop('warnings', None) 180 if warnings is not None: 181 self.flush({'warn': warnings}, options) 182 self.flush(logdata, options) 183 184 def v2_playbook_on_start(self, playbook): 185 self.playbook = playbook 186 self.playbook_name = playbook._file_name 187 188 def v2_playbook_on_stats(self, stats): 189 result = dict() 190 for host in stats.processed.keys(): 191 result[host] = stats.summarize(host) 192 self.sendLog(self.conf_hostname, 'STATS', {'info': self.sanitizeJSON(result)}) 193 194 def runner_on_failed(self, host, res, ignore_errors=False): 195 if self.plugin_ignore_errors: 196 ignore_errors = self.plugin_ignore_errors 197 self.sendLog(host, 'FAILED', {'info': self.sanitizeJSON(res), 'ignore_errors': ignore_errors}) 198 199 def runner_on_ok(self, host, res): 200 self.sendLog(host, 'OK', {'info': self.sanitizeJSON(res)}) 201 202 def runner_on_unreachable(self, host, res): 203 self.sendLog(host, 'UNREACHABLE', {'info': self.sanitizeJSON(res)}) 204 205 def runner_on_async_failed(self, host, res, jid): 206 self.sendLog(host, 'ASYNC_FAILED', {'info': self.sanitizeJSON(res), 'job_id': jid}) 207 208 def runner_on_async_ok(self, host, res, jid): 209 self.sendLog(host, 'ASYNC_OK', {'info': self.sanitizeJSON(res), 'job_id': jid}) 210