From 20b83610b40dfe30807dd245ee5844bebbe49886 Mon Sep 17 00:00:00 2001 From: Seth Vidal Date: Fri, 28 Jun 2013 20:59:12 +0000 Subject: [PATCH] add logdetail callback plugin - hopefully this will not break for us --- callback_plugins/logdetail.py | 256 ++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 callback_plugins/logdetail.py diff --git a/callback_plugins/logdetail.py b/callback_plugins/logdetail.py new file mode 100644 index 0000000000..8198377972 --- /dev/null +++ b/callback_plugins/logdetail.py @@ -0,0 +1,256 @@ +# (C) 2012, Michael DeHaan, + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import time +import json +from ansible import utils + +TIME_FORMAT="%b %d %Y %H:%M:%S" + +MSG_FORMAT="%(now)s - %(count)s - %(category)s - %(name)s - %(data)s\n" + +LOG_PATH = '/var/log/ansible' + + +class LogMech(object): + def __init__(self): + self.started = time.time() + self._pb_fn = None + self._last_task_start = None + self.play_info = {} + self.logpath = LOG_PATH + if not os.path.exists(self.logpath): + try: + os.makedirs(self.logpath) + except OSError, e: + if e.errno != 17: + raise + + # checksum of full playbook? + + @property + def playbook_id(self): + if self._pb_fn: + return os.path.basename(self._pb_fn).replace('.yml', '').replace('.yaml', '') + else: + return "Unknown-playbook" + + @playbook_id.setter + def playbook_id(self, value): + self._pb_fn = value + + @property + def logpath_play(self): + # this is all to get our path to look nice ish + day = time.strftime('%Y/%m/%d', time.localtime(self.started)) + offset_in_sec = str(self.started - time.mktime(time.strptime(day, '%Y/%m/%d'))) + path = os.path.normpath(self.logpath + '/' + day + '/' + self.playbook_id + '/' + offset_in_sec) + + if not os.path.exists(path): + try: + os.makedirs(path) + except OSError, e: + if e.errno != 17: # if it is not dir exists then raise it up + raise + + return path + + def play_log(self, content): + # record out playbook.log + # include path to playbook, checksums, user running playbook + # any args we can get back from the invocation + fd = open(self.logpath_play + '/' + 'playbook.log', 'a') + fd.write('%s\n' % content) + fd.close() + + def task_to_json(self, task): + res = {} + res['task_name'] = task.name + res['task_module'] = task.module_name + res['task_args'] = task.module_args + for k in ("delegate_to", "environment", "first_available_file", + "local_action", "notified_by", "notify", "only_if", + "register", "sudo", "sudo_user", "tags", + "transport", "when"): + v = getattr(task, k, None) + if v: + res['task_' + k] = v + + return res + + def log(self, host, category, data, task=None, count=0): + if not host: + host = 'HOSTMISSING' + + name = data.get('module_name',None) + + + # we're in setup - move the invocation info up one level + if 'invocation' in data: + invoc = data['invocation'] + if not name and 'module_name' in invoc: + name = invoc['module_name'] + + del(data['invocation']) + data.update(invoc) + + if task: + name = task.name + data['task_start'] = self._last_task_start + data['task_end'] = time.time() + data.update(self.task_to_json(task)) + + if category == 'OK' and data.get('changed', False): + category = 'CHANGED' + + + fd = open(self.logpath_play + '/' + host + '.log', 'a') + now = time.strftime(TIME_FORMAT, time.localtime()) + fd.write(MSG_FORMAT % dict(now=now, name=name, count=count, category=category, data=json.dumps(data))) + fd.close() + + +logmech = LogMech() + +class CallbackModule(object): + """ + logs playbook results, per host, in /var/log/ansible/hosts + """ + def __init__(self): + self._task_count = 0 + self._play_count = 0 + + def on_any(self, *args, **kwargs): + pass + + + def runner_on_failed(self, host, res, ignore_errors=False): + category = 'FAILED' + task = getattr(self,'task', None) + logmech.log(host, category, res, task, self._task_count) + + + def runner_on_ok(self, host, res): + category = 'OK' + task = getattr(self,'task', None) + logmech.log(host, category, res, task, self._task_count) + + + def runner_on_error(self, host, res): + category = 'ERROR' + task = getattr(self,'task', None) + logmech.log(host, category, res, task, self._task_count) + + def runner_on_skipped(self, host, item=None): + category = 'SKIPPED' + task = getattr(self,'task', None) + res = {'item':item} + logmech.log(host, category, res, task, self._task_count) + + def runner_on_unreachable(self, host, res): + category = 'UNREACHABLE' + task = getattr(self,'task', None) + logmech.log(host, category, res, task, self._task_count) + + def runner_on_no_hosts(self): + pass + + def runner_on_async_poll(self, host, res, jid, clock): + pass + + def runner_on_async_ok(self, host, res, jid): + pass + + def runner_on_async_failed(self, host, res, jid): + category = 'ASYNC_FAILED' + task = getattr(self,'task', None) + logmech.log(host, category, res, task, self._task_count) + + def playbook_on_start(self): + pass + + def playbook_on_notify(self, host, handler): + pass + + def playbook_on_no_hosts_matched(self): + pass + + def playbook_on_no_hosts_remaining(self): + pass + + def playbook_on_task_start(self, name, is_conditional): + logmech._last_task_start = time.time() + self._task_count += 1 + + def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None): + pass + + def playbook_on_setup(self): + self._task_count += 1 + pass + + def playbook_on_import_for_host(self, host, imported_file): + task = getattr(self,'task', None) + logmech.log(host, 'IMPORTED', imported_file, task) + + def playbook_on_not_import_for_host(self, host, missing_file): + task = getattr(self,'task', None) + logmech.log(host, 'NOTIMPORTED', missing_file, task) + + def playbook_on_play_start(self, pattern): + self._task_count = 0 + + play = getattr(self, 'play', None) + if play: + # figure out where the playbook FILE is + path = os.path.abspath(play.playbook.filename) + + # tel the logger what the playbook is + logmech.playbook_id = path + + # if play count == 0 + # write out playbook info now + if not self._play_count: + pb_info = {} + pb_info['playbook'] = path + pb_info['userid'] = os.getlogin() + pb_info['extra_vars'] = play.playbook.extra_vars + pb_info['inventory'] = play.playbook.inventory.host_list + pb_info['playbook_checksum'] = utils.md5(path) + logmech.play_log(json.dumps(pb_info, indent=4)) + + self._play_count += 1 + # then write per-play info that doesn't duplcate the playbook info + info = {} + info['play'] = play.name + info['hosts'] = play.hosts + info['transport'] = play.transport + info['number'] = self._play_count + logmech.play_info = info + logmech.play_log(json.dumps(info, indent=4)) + + + def playbook_on_stats(self, stats): + results = {} + for host in stats.processed.keys(): + results[host] = stats.summarize(host) + logmech.log(host, 'STATS', results[host]) + logmech.play_log(json.dumps({'stats': results}, indent=4)) + print ' logs written to: %s\n' % logmech.logpath_play + +