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)
|