Projet

Général

Profil

Demande #3021 » check_qshape.py

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

 
1
#!/usr/bin/env python3
2

    
3
#   Copyright (C) 2016  François Poulain <fpoulain@metrodore.fr>
4
#
5
#   This program is free software: you can redistribute it and/or modify
6
#   it under the terms of the GNU General Public License as published by
7
#   the Free Software Foundation, either version 3 of the License, or
8
#   (at your option) any later version.
9
#
10
#   This program is distributed in the hope that it will be useful,
11
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
#   GNU General Public License for more details.
14
#
15
#   You should have received a copy of the GNU General Public License
16
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

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

    
22
Scoring method: sum of minutes of each deferred emails.
23
"""
24

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

    
40
import argparse, re
41
from subprocess import Popen, PIPE
42

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

    
45
# ======================================
46
# Argument parsing
47
# ======================================
48

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

    
56
class AppendAction(argparse.Action):
57
    def __call__(self, parser, namespace, values, option_string=None):
58
        setattr(namespace, self.dest, values)
59
        call.append (self.dest)
60
        if isinstance (values, int):
61
            call.append (str(values))
62
        else:
63
            call.append (values)
64

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

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

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

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

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

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

    
106
args = parser.parse_args()
107
call.append (args.queue)
108

    
109
# ======================================
110
# Qshape call and data processing
111
# ======================================
112

    
113
def parse_cell(s):
114
    try:
115
        return int (s)
116
    except ValueError:
117
        return s
118

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

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

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

    
132
# ======================================
133
# Main call
134
# ======================================
135

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

    
138
try:
139
    with Popen (call, stdout=PIPE, universal_newlines= True) as qs:
140
        out= str (qs.stdout.read())
141
        ts= scores (parse_tab (out))
142
        score = ts[0][1]
143
        perfsdata= 'score={};{};{};;'.format(score, args.warning, args.critical)
144
        if score > args.critical:
145
            status= 2
146
        elif score > args.warning:
147
            status= 1
148
        else:
149
            status= 0
150
        for domain, score in ts[1:args.number]:
151
            if args.domains and domain in args.domains and score > args.warning:
152
                status= 1
153
        print ('QSHAPE:', status_output[status], 'Total score:', score, ts[1:args.number+1], '|', perfsdata)
154
        if args.verbose > 1:
155
            print(out)
156
        if args.verbose > 0:
157
            for domain, score in ts:
158
                print (domain.rjust(35), score)
159
except:
160
    print ('Unable to exec: ', ' '.join(call), ' Is qshape installed?')
161
    exit (3)
162

    
163
exit (status)
(1-1/2)