Projet

Général

Profil

Demande #3021 » check_qshape.py

François Poulain, 06/09/2019 14:09

 
#!/usr/bin/env python3

# Copyright (C) 2016 François Poulain <fpoulain@metrodore.fr>
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.

"""
Read qshape output and attempt to score mailqueue in order to detect
delivrability issues and impacted domains.

Scoring method: sum of minutes of each deferred emails.
"""

# TODO:
# X catcher la sortie de qshape
# X intégrer options et contraintes
# X intégrer option -t
# X intégrer option -b
# X intégrer option -l
# X intégrer option -s
# X intégrer option -v -vv et -vvv https://nagios-plugins.org/doc/guidelines.html#DEVREQUIREMENTS
# X intégrer option -V
# X intégrer option -C -W
# X sortir perfdatas sur le total
# X afficher -N pire(s) domaines ou senders
# X intégrer (-D) domaines particuliers qui lèvent des warnings
# X intégrer codes exit (0 ok, 1 wanr, 2 crit, 3 unknwon)

import argparse, re
from subprocess import Popen, PIPE

call= ['/usr/sbin/qshape']

# ======================================
# Argument parsing
# ======================================

class ConstraintsAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
vals= vars(namespace)
if vals['warning'] and vals['critical'] and vals['warning'] > vals['critical']:
raise argparse.ArgumentError (self, 'critical threshold should be greater than warning threshold')

class AppendAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
call.append (self.dest)
if isinstance (values, int):
call.append (str(values))
else:
call.append (values)

class AppendFlagAction(argparse._StoreTrueAction):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
call.append (self.dest)

parser = argparse.ArgumentParser(description="""
Read qshape output and attempt to score mailqueue in order to detect
delivrability issues and impacted domains.
""")

ma= parser.add_argument_group(title='mandatory arguments', description=None)
ma.add_argument('-C', '--critical', metavar='THRESH', type=int, required= True,
action=ConstraintsAction, help='Critical threshold')
ma.add_argument('-W', '--warning', metavar='THRESH', type=int, required= True,
action=ConstraintsAction, help='Warning threshold')

parser.add_argument('-Q', '--queue', metavar='QUEUE_NAME', default='deferred',
choices=['deferred', 'active', 'hold', 'incoming', 'maildrop'],
help='Queue name. default is deferred.')
parser.add_argument('-D', '--domains', metavar='DOMAIN.TLD', type=str, nargs='+',
help='Warn when given domain(s) appears in displayed bottlenecked domains.')
parser.add_argument('-N', '--number', metavar='INT', type=int, default= 3,
help='Number of displayed bottlenecked domains. Default is 3.')
parser.add_argument('-v', '--verbose', default=0, action='count',
help='Verbose output. -vv for very verbose output')
parser.add_argument('-V', '--version', action='version', version='%(prog)s 0.1')

qs= parser.add_argument_group(title='qshape arguments', description='Arguments passed to qshape')

qs.add_argument('-b', '--buckets', metavar='INT', type=int, action=AppendAction, dest='-b',
help='''The age distribution is broken up into a sequence of geometrically
increasing intervals. This option sets the number of intervals or "buckets".
Each bucket has a maximum queue age that is twice as large as that of the
previous bucket. The last bucket has no age limit. Default is 10.''')
qs.add_argument('-l', '--linear', action=AppendFlagAction, dest='-l',
help='Instead of using a geometric age sequence, use a linear age sequence.')
qs.add_argument('-s', '--senders', action=AppendFlagAction, dest='-s',
help='Display the sender domain distribution instead of the recipient domain distribution.')
qs.add_argument('-t', '--time', metavar='MINUTES', type=int, action=AppendAction, dest='-t',
help='The age limit in minutes for the first time bucket. Default is 5.')

args = parser.parse_args()
call.append (args.queue)

# ======================================
# Qshape call and data processing
# ======================================

def parse_cell(s):
try:
return int (s)
except ValueError:
return s

def parse_lines(l):
return [parse_cell(s) for s in l]

def parse_tab (s):
return [parse_lines (re.split(' +', line)[1:]) for line in re.split('\n', s)]

def scores (t):
head= [0] + t[0][1:-1]
data= t[1:-1]
ts= [[line[0], sum (c * t for c, t in zip (head, line[1:]))] for line in data]
ts.sort(key=lambda x: x[1], reverse=True)
return ts

# ======================================
# Main call
# ======================================

status_output= ['OK', 'WARN', 'CRIT', 'UNK']

try:
with Popen (call, stdout=PIPE, universal_newlines= True) as qs:
out= str (qs.stdout.read())
ts= scores (parse_tab (out))
score = ts[0][1]
perfsdata= 'score={};{};{};;'.format(score, args.warning, args.critical)
if score > args.critical:
status= 2
elif score > args.warning:
status= 1
else:
status= 0
for domain, score in ts[1:args.number]:
if args.domains and domain in args.domains and score > args.warning:
status= 1
print ('QSHAPE:', status_output[status], 'Total score:', score, ts[1:args.number+1], '|', perfsdata)
if args.verbose > 1:
print(out)
if args.verbose > 0:
for domain, score in ts:
print (domain.rjust(35), score)
except:
print ('Unable to exec: ', ' '.join(call), ' Is qshape installed?')
exit (3)

exit (status)
(1-1/2)