225 lines
8.2 KiB
Python
Executable file
225 lines
8.2 KiB
Python
Executable file
#!/usr/bin/python
|
|
# vim: et ts=4 ai sw=4 sts=0
|
|
import sys
|
|
import json
|
|
from optparse import OptionParser
|
|
import os
|
|
import glob
|
|
import gzip
|
|
from datetime import datetime, date, timedelta
|
|
import dateutil.parser as dateparser
|
|
import configparser
|
|
from ansible.config.manager import find_ini_config_file
|
|
from collections import Counter
|
|
|
|
logpath = '/var/log/ansible'
|
|
search_terms = ['CHANGED', 'FAILED']
|
|
date_terms = {
|
|
"today": date.today,
|
|
"yesterday": lambda: date.today() - timedelta(1),
|
|
}
|
|
|
|
class bcolors:
|
|
BLUE = '\033[94m'
|
|
GREEN = '\033[92m'
|
|
WARNING = '\033[93m'
|
|
FAIL = '\033[91m'
|
|
ENDC = '\033[0m'
|
|
ERROR = '\033[41m'
|
|
WHITE = '\033[00m'
|
|
|
|
|
|
def format_state(category, txt=None):
|
|
if not txt:
|
|
txt = category
|
|
if category == "OK":
|
|
color_out = bcolors.GREEN + txt + bcolors.ENDC
|
|
elif category == "FAILED":
|
|
color_out = bcolors.FAIL + txt + bcolors.ENDC
|
|
elif category == "CHANGED":
|
|
color_out = bcolors.WARNING + txt + bcolors.ENDC
|
|
elif category == "SKIPPED":
|
|
color_out = bcolors.BLUE + txt + bcolors.ENDC
|
|
elif category == "UNREACHABLE":
|
|
color_out = bcolors.ERROR + txt + bcolors.ENDC
|
|
else:
|
|
color_out = bcolors.WHITE + txt + bcolors.ENDC
|
|
return color_out
|
|
|
|
|
|
def parse_info(infofile):
|
|
data = {}
|
|
with open(infofile) as f:
|
|
content = f.read()
|
|
obj_list = [x+'}' for x in content.split('\n}')]
|
|
plays = []
|
|
for obj in obj_list[:-1]:
|
|
js = json.loads(obj)
|
|
if 'play' in js:
|
|
plays.append(js)
|
|
else:
|
|
data.update(json.loads(obj))
|
|
data['plays'] = plays
|
|
return data
|
|
|
|
|
|
def date_cheat(datestr):
|
|
dc = date_terms.get(datestr, lambda: dateparser.parse(datestr))
|
|
return dc()
|
|
|
|
|
|
def parse_args(args):
|
|
usage = """
|
|
logview [options] [-d datestr] [-p playbook]
|
|
|
|
examples:
|
|
logview -d yesterday -l # lists playbooks run on that date
|
|
|
|
logview -s OK -s FAILED -d yesterday # list events from yesterday that failed or were ok
|
|
|
|
logview -s CHANGED -d yesterday -p mirrorlist # list events that changed from the mirrorlist playbook
|
|
|
|
logview -s ANY -d yesterday -p mirrorlist # list all events from the mirrorlist playbook
|
|
|
|
"""
|
|
parser = OptionParser(usage=usage)
|
|
parser.add_option("-d", default='today', dest='datestr', help="time string of when you want logs")
|
|
parser.add_option("-p", default='*', dest='playbook', help="the playbook you want to look for")
|
|
parser.add_option("-H", default=[], dest='hostname', action='append', help="Limit to the specified hostname")
|
|
parser.add_option("-m", default=False, dest='message', action='store_true', help='Show tasks output')
|
|
parser.add_option("-v", default=False, dest='verbose', action='store_true', help='Verbose')
|
|
parser.add_option("-s", default=[], dest='search_terms', action='append', help="status to search for")
|
|
parser.add_option("-l", default=False, dest="list_pb", action='store_true', help="list playbooks for a specific date")
|
|
parser.add_option("-L", default=False, dest="list_all_pb", action='store_true', help="list all playbooks ever ran")
|
|
parser.add_option("--profile", default=False, dest="profile", action='store_true', help="output timing input per task")
|
|
(opts, args) = parser.parse_args(args)
|
|
|
|
opts.datestr = date_cheat(opts.datestr)
|
|
if not opts.search_terms:
|
|
opts.search_terms = search_terms
|
|
return opts, args
|
|
|
|
|
|
def search_logs(opts, logfiles):
|
|
msg = ''
|
|
for fn in sorted(logfiles):
|
|
hostname = os.path.basename(fn).replace('.log', '').replace('.gz', '')
|
|
timestamp = os.path.basename(os.path.dirname(fn))
|
|
|
|
if opts.hostname and hostname not in opts.hostname:
|
|
continue
|
|
|
|
try:
|
|
with gzip.open(fn) as f:
|
|
f.read()
|
|
open_f = gzip.open(fn, "rt")
|
|
except:
|
|
open_f = open(fn)
|
|
|
|
for line in open_f:
|
|
things = line.split('\t')
|
|
if len(things) < 5:
|
|
msg += "(logview error - unhandled line): %r\n" % line
|
|
continue
|
|
|
|
# See callback_plugins/logdetail.py for how these lines get created.
|
|
# MSG_FORMAT="%(now)s\t%(count)s\t%(category)s\t%(name)s\t%(data)s\n"
|
|
task_ts, count, category, name, data = things
|
|
|
|
if category in opts.search_terms or 'ANY' in opts.search_terms:
|
|
dur = None
|
|
slurp = json.loads(data)
|
|
if opts.profile:
|
|
st = slurp.get('task_start', 0)
|
|
end = slurp.get('task_end', 0)
|
|
if st and end:
|
|
dur = '%.2fs' % (float(end) - float(st))
|
|
|
|
state = format_state(category)
|
|
c_hostname = format_state(category, hostname)
|
|
|
|
if category == "STATS":
|
|
if type(slurp) == dict:
|
|
# ok=2 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
|
|
name = 'o:%s c:%s u:%s f:%s s:%s r:%s i:%s' % (
|
|
slurp.get("ok", 0),
|
|
slurp.get("changed", 0),
|
|
slurp.get("unreachable", 0),
|
|
slurp.get("failures", 0),
|
|
slurp.get("skipped", 0),
|
|
slurp.get("rescued", 0),
|
|
slurp.get("ignored", 0))
|
|
|
|
if not name:
|
|
name = slurp.get("task_module")
|
|
|
|
msg += '{0: <10} {1: <30} {2: <22} {3: <4} {4: <25}'.format(
|
|
timestamp, c_hostname, task_ts, count, state)
|
|
|
|
if dur:
|
|
msg += ' {0: <8}'.format(dur,)
|
|
|
|
if name:
|
|
msg += ' %s' % (name,)
|
|
|
|
if not opts.verbose:
|
|
if type(slurp) == dict:
|
|
for term in ['cmd',]:
|
|
if term in slurp:
|
|
msg += '\t%s:%s' % (term, slurp.get(term, None))
|
|
|
|
if opts.message:
|
|
for term in ['msg',]:
|
|
if term in slurp:
|
|
value = slurp.get(term, None).strip()
|
|
if value:
|
|
msg += '\n%s: %s\n' % (term, format_state(category, value))
|
|
msg += '\n'
|
|
else:
|
|
msg += '\n'
|
|
msg += json.dumps(slurp, indent=4)
|
|
msg += '\n'
|
|
|
|
return msg
|
|
|
|
|
|
def main(args):
|
|
cfg = find_ini_config_file()
|
|
if cfg:
|
|
cp = configparser.ConfigParser()
|
|
cp.read(cfg)
|
|
logpath = cp.get('callback_logdetail', "log_path", fallback="/var/log/ansible")
|
|
opts, args = parse_args(args)
|
|
|
|
if opts.list_pb or opts.list_all_pb:
|
|
for r,d,f in os.walk(logpath):
|
|
for file in f:
|
|
if file.endswith('.info'):
|
|
pb = parse_info(os.path.join(r,file))
|
|
pb_name = os.path.splitext(os.path.basename(pb['playbook']))[0]
|
|
pb_date = datetime.fromtimestamp(pb['playbook_start'])
|
|
if opts.list_all_pb or opts.datestr == pb_date.date():
|
|
stats = Counter()
|
|
for stat in pb['stats'].values():
|
|
del stat['task_userid']
|
|
stats += Counter(stat)
|
|
|
|
print("%s\tplaybook:%s\tran by:%s\tsummary: ok:%s chd:%s unr:%s faild:%s"
|
|
% (pb_date, pb_name, pb['userid'],
|
|
stats['ok'], stats['changed'], stats['unreachable'],
|
|
stats['failures']))
|
|
|
|
|
|
else:
|
|
for pb in glob.glob(os.path.join(logpath, opts.playbook)):
|
|
pb_name = os.path.basename(pb)
|
|
for pb_logdir in glob.glob(os.path.join(pb, opts.datestr.strftime("%Y/%m/%d"))):
|
|
logfiles = glob.glob(pb_logdir + '/*/*.log*')
|
|
msg = search_logs(opts, logfiles)
|
|
if msg:
|
|
print(pb_name)
|
|
print(msg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|