Browse Source

Initial commit

Yann Weber 1 month ago
commit
b76fa52c45
4 changed files with 457 additions and 0 deletions
  1. 14
    0
      copying
  2. 440
    0
      rspamd_
  3. 1
    0
      rspamd_.py
  4. 2
    0
      tests.sh

+ 14
- 0
copying View File

@@ -0,0 +1,14 @@
1
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+                    Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+  0. You just DO WHAT THE FUCK YOU WANT TO.
14
+

+ 440
- 0
rspamd_ View File

@@ -0,0 +1,440 @@
1
+#!/usr/bin/env python3
2
+# %# family=auto
3
+# %# capabilities=autoconf suggest
4
+
5
+"""
6
+Rpsamd munin plugin
7
+
8
+Can be use as
9
+- rspamd_actions
10
+- rspamd_results
11
+- rspamd_scantime
12
+- rspamd_uptime
13
+- rspamd_memory_usage
14
+- rspamd_memory_stats
15
+- rspamd_connections
16
+- rspamd_ctrl_connections
17
+- rspamd_statfiles
18
+- rspamd_fuzzy_hashes
19
+
20
+Config :
21
+
22
+[rspamd_*]
23
+env.rspamc = "/usr/bin/rspamc"
24
+env.cache_file = "/tmp/munin_rspamd.cache"
25
+env.cache_time = 240
26
+"""
27
+
28
+import json
29
+import os
30
+import re
31
+import shlex
32
+import sys
33
+import time
34
+import io
35
+import unittest
36
+import unittest.mock
37
+import tempfile
38
+import contextlib
39
+
40
+
41
+def usage():
42
+    """Return simple help"""
43
+    return """You should ln -s /usr/share/munin/plugins/rspamd_ /etc/munin/plugins/rspamd_FUNC
44
+With FUNC one of %s""" % (
45
+        ", ".join(CONFIGS.keys())
46
+    )
47
+
48
+
49
+def avg_scantime(scantimes):
50
+    """Returns average, min, max scantimes"""
51
+    total = [float(t) for t in scantimes if t]
52
+    return sum(total) / len(total), min(total), max(total)
53
+
54
+
55
+def storage2name(name):
56
+    """Format storage name"""
57
+    return re.sub(r"[\.\- ]", "_", name)
58
+
59
+
60
+CONFIGS = {
61
+    "actions": [
62
+        lambda data: """\
63
+graph_title Rspamd actions
64
+graph_vlabel mails
65
+graph_args --base 1000 -l 0
66
+graph_category antispam
67
+graph_scale yes
68
+scanned.label Scanned/total
69
+learned.label Learned
70
+%s"""
71
+        % (
72
+            "\n".join(
73
+                [
74
+                    "%s.label %s" % (action.replace(" ", "_"), action.title())
75
+                    for action in data["actions"]
76
+                ]
77
+            )
78
+        ),
79
+        lambda data: """\
80
+scanned.value %d
81
+learned.value %d
82
+%s"""
83
+        % (
84
+            data["scanned"],
85
+            data["learned"],
86
+            "\n".join(
87
+                [
88
+                    "%s.value %d" % (action.replace(" ", "_"), value)
89
+                    for action, value in data["actions"].items()
90
+                ]
91
+            ),
92
+        ),
93
+    ],
94
+    "results": [
95
+        """\
96
+graph_title Rspamd results
97
+graph_vlabel mails
98
+graph_args --base 1000 -l 0
99
+graph_category antispam
100
+graph_scale yes
101
+spam.label Spams
102
+spam.draw AREA
103
+ham.label Ham
104
+ham.draw STACK""",
105
+        lambda data: """\
106
+spam.value %d
107
+ham.value %d"""
108
+        % (data["spam_count"], data["ham_count"]),
109
+    ],
110
+    "scantime": [
111
+        """\
112
+graph_title Rspamd scantime
113
+graph_args --base 1000 -l 0
114
+graph_category antispam
115
+graph_scale yes
116
+graph_vlabel seconds
117
+scantime.label Average scantime
118
+min.label Minimum scantime
119
+max.label Maximum scantime
120
+""",
121
+        lambda data: """\
122
+scantime.value %f
123
+min.value %f
124
+max.value %f"""
125
+        % avg_scantime(data["scan_times"]),
126
+    ],
127
+    "uptime": [
128
+        """\
129
+graph_title Rspamd uptime
130
+graph_args --base 1000 -l 0
131
+graph_category antispam
132
+graph_scale no
133
+graph_vlabel uptime in days
134
+uptime.label uptime
135
+uptime.draw AREA
136
+""",
137
+        lambda data: "uptime.value %f" % (data["uptime"] / (3600 * 24)),
138
+    ],
139
+    "memory_usage": [
140
+        """\
141
+graph_title Rspamd memory usage
142
+graph_args --base 1024 -l 0
143
+graph_category antispam
144
+graph_scale yes
145
+graph_vlabel bytes
146
+memory.label Memory usage
147
+""",
148
+        lambda data: "memory.value %d" % data["bytes_allocated"],
149
+    ],
150
+    "memory_stats": [
151
+        """\
152
+graph_title Rspamd memory stats
153
+graph_args --base 1000 -l 0
154
+graph_category antispam
155
+graph_scale yes
156
+pools.label Pools allocated - freed
157
+chunks.label Chunks allocated - freed
158
+shared_chunks_allocated.label Shared chunks allocated
159
+""",
160
+        lambda data: """\
161
+pools.value %d
162
+chunks.value %d
163
+shared_chunks_allocated.value %d"""
164
+        % (
165
+            data["pools_allocated"] - data["pools_freed"],
166
+            data["chunks_allocated"] - data["chunks_freed"],
167
+            data["shared_chunks_allocated"],
168
+        ),
169
+    ],
170
+    "connections": [
171
+        """\
172
+graph_title Rspamd connections
173
+graph_args --base 1000 -l 0
174
+graph_category antispam
175
+graph_scale yes
176
+graph_vlabel connections
177
+connections.label opened connections
178
+""",
179
+        lambda data: "connections.value %d" % data["connections"],
180
+    ],
181
+    "ctrl_connections": [
182
+        """\
183
+graph_title Rspamd control connections count
184
+graph_args --base 1000 -l 0
185
+graph_category antispam
186
+graph_scale yes
187
+graph_vlabel connections
188
+connections.label control connections count
189
+""",
190
+        lambda data: "connections.value %d" % data["control_connections"],
191
+    ],
192
+    "statfiles": [
193
+        lambda data: """\
194
+graph_title rspamd bayes statfiles
195
+graph_args --base 1000 -l 0
196
+graph_scale yes
197
+graph_category antispam
198
+graph_vlabel learned mails
199
+%s"""
200
+        % (
201
+            "\n".join(
202
+                [
203
+                    """learned_{sym}.label {sym} learned
204
+users_{sym}.label {sym} users""".format(
205
+                        sym=stat["symbol"]
206
+                    )
207
+                    for stat in data["statfiles"]
208
+                ]
209
+            )
210
+        ),
211
+        lambda data: "\n".join(
212
+            """\
213
+learned_{sym}.value {lv}
214
+users_{sym}.value {uv}""".format(
215
+                sym=stat["symbol"], lv=stat["revision"], uv=stat["users"]
216
+            )
217
+            for stat in data["statfiles"]
218
+        ),
219
+    ],
220
+    "fuzzy_hashes": [
221
+        lambda data: """\
222
+graph_title rspamd fuzzy hases storage
223
+graph_args --base 1000 -l 0
224
+graph_scale yes
225
+graph_category antispam
226
+graph_vlabel hashes
227
+%s"""
228
+        % (
229
+            "\n".join(
230
+                [
231
+                    "%s.label %s" % (storage2name(storage), storage)
232
+                    for storage in data["fuzzy_hashes"]
233
+                ]
234
+            )
235
+        ),
236
+        lambda data: "\n".join(
237
+            [
238
+                "%s.value %d" % (storage2name(storage), count)
239
+                for storage, count in data["fuzzy_hashes"].items()
240
+            ]
241
+        ),
242
+    ],
243
+}
244
+
245
+
246
+def main(argv, environ):
247
+    """main function"""
248
+    rspamc = environ.get("rspamc", "/usr/bin/rspamc")
249
+    cache_file = environ.get("cache_file", "/tmp/munin_rspamd.cache")
250
+    cache_time = int(environ.get("cache_time", 60 * 4))
251
+
252
+    rspamc_cmd = shlex.join([rspamc, "--json", "stat"])
253
+
254
+    if len(argv) > 1:
255
+        if argv[1] == "autoconf":
256
+            if os.path.exists(rspamc):
257
+                return "yes"
258
+            return "no (%s not found)" % rspamc
259
+        if argv[1] == "suggest":
260
+            return "#./rspamd_ suggest\n" + ("\n".join(CONFIGS.keys()))
261
+
262
+    data = None
263
+    if os.path.exists(cache_file):
264
+        cache_stat = os.stat(cache_file)
265
+        if cache_stat.st_mtime > time.time() - cache_time:
266
+            with open(cache_file, "r", encoding="utf-8") as cache:
267
+                try:
268
+                    data = json.loads(cache.read())
269
+                except json.JSONDecodeError:
270
+                    pass
271
+
272
+    if data is None:
273
+        with open(cache_file, "w", encoding="utf-8") as cache:
274
+            with os.popen(rspamc_cmd, mode="r") as pipe:
275
+                data = pipe.read()
276
+                cache.write(data)
277
+                data = json.loads(data)
278
+
279
+    spl = argv[0].split("_")[1:]
280
+    if len(spl) == 0:
281
+        raise NameError(usage())
282
+    cmd = "_".join(spl)
283
+    if cmd not in CONFIGS:
284
+        raise NameError("Unknown command %r. %s" % (cmd, usage()))
285
+
286
+    config = CONFIGS[cmd][0]
287
+    act = CONFIGS[cmd][1]
288
+
289
+    if len(argv) > 1 and argv[1] == "config":
290
+        if not isinstance(config, str):
291
+            config = config(data)
292
+        return config
293
+
294
+    return act(data)
295
+
296
+
297
+if __name__ == "__main__":
298
+    try:
299
+        print(main(sys.argv, os.environ))
300
+    except NameError as main_expt:
301
+        print(main_expt)
302
+
303
+#
304
+# Tests
305
+#
306
+
307
+JSON = r"""{\
308
+"version":"3.4","config_id":"1234","uptime":16991,"read_only":false,\
309
+"scanned":43,"learned":43,"actions":\
310
+{"reject":0,"soft reject":0,"rewrite subject":0,"add header":5,\
311
+"greylist":4,"no action":34},"scan_times":\
312
+[0.910916,1.291352,1.241323,null,null,null,null,null,null,null,\
313
+null,null,null,null,null,null,null,null,null,null,null,null,null\
314
+,null,null,null,null,null,null,null,null],\
315
+"spam_count":5,"ham_count":38,"connections":2,\
316
+"control_connections":265,"pools_allocated":538,"pools_freed":512,\
317
+"bytes_allocated":27817136,"chunks_allocated":110,\
318
+"shared_chunks_allocated":3,"chunks_freed":0,"chunks_oversized":1,\
319
+"fragmented":0,"total_learns":40,"statfiles":\
320
+[{"revision":33,"used":0,"total":0,"size":0,"symbol":"BAYES_SPAM",\
321
+"type":"redis","languages":0,"users":1},\
322
+{"revision":7,"used":0,"total":0,"size":0,"symbol":"BAYES_HAM",\
323
+"type":"redis","languages":0,"users":1}],\
324
+"fuzzy_hashes":{"local":0,"rspamd.com":1872513971},"scan_time":0.02}"""
325
+
326
+EXPTS = {
327
+    "actions": [
328
+        "reject.value 0",
329
+        "soft_reject.value 0",
330
+        "rewrite_subject.value 0",
331
+        "add_header.value 5",
332
+        "greylist.value 4",
333
+        "no_action.value 34",
334
+        "learned.value 43",
335
+        "scanned.value 43",
336
+    ],
337
+    "results": ["spam.value 5", "ham.value 38"],
338
+    "scantime": ["scantime.value 1.147864", "min.value 0.910916", "max.value 1.291352"],
339
+    "uptime": [
340
+        "uptime.value 0.196655",
341
+    ],
342
+    "memory_usage": [
343
+        "memory.value 27817136",
344
+    ],
345
+    "memory_stats": [
346
+        "chunks.value 110",
347
+        "shared_chunks_allocated.value 3",
348
+        "pools.value 26",
349
+    ],
350
+    "connections": ["connections.value 2"],
351
+    "ctrl_connections": ["connections.value 265"],
352
+    "statfiles": [
353
+        "users_BAYES_SPAM.value 1",
354
+        "learned_BAYES_HAM.value 7",
355
+        "learned_BAYES_SPAM.value 33",
356
+        "users_BAYES_HAM.value 1",
357
+    ],
358
+    "fuzzy_hashes": ["local.value 0", "rspamd_com.value 1872513971"],
359
+}
360
+
361
+
362
+@contextlib.contextmanager
363
+def json_sample():
364
+    """Mock rspamc --json stat result"""
365
+    return io.StringIO(JSON)
366
+
367
+
368
+class MuninRspamdTests(unittest.TestCase):
369
+    """Plugin selftest"""
370
+
371
+    def setUp(self):
372
+        """Prepare cache file & environment"""
373
+        self.json = lambda: io.StringIO(JSON)
374
+        tmpfd, self.tmpcache = tempfile.mkstemp()
375
+        os.close(tmpfd)
376
+        os.unlink(self.tmpcache)
377
+        self.environ = {"cache_file": self.tmpcache}
378
+
379
+    def tearDown(self):
380
+        """Remove cache file"""
381
+        if os.path.isfile(self.tmpcache):
382
+            os.unlink(self.tmpcache)
383
+
384
+    def test_configs(self):
385
+        """check plugin config runs without error and returns
386
+        expected fields
387
+        """
388
+        for cmd in CONFIGS:
389
+            with self.subTest(cmd=cmd):
390
+                with unittest.mock.patch("os.popen", return_value=self.json()):
391
+                    ret = main(["rspamd_%s" % cmd, "config"], self.environ)
392
+                    self.assertIn("graph_title", ret)
393
+                    self.assertIn("graph_args", ret)
394
+                    self.assertIn("graph_scale", ret)
395
+                    self.assertIn("graph_category antispam", ret)
396
+                self.tearDown()
397
+
398
+    def test_values(self):
399
+        """check plugin runs without error"""
400
+        for cmd in CONFIGS:
401
+            with self.subTest(cmd=cmd):
402
+                with unittest.mock.patch("os.popen", return_value=self.json()):
403
+                    ret = main(["rspamd_%s" % cmd], self.environ)
404
+                    self.assertNotEqual(len(ret), 0)
405
+
406
+    def test_consistency(self):
407
+        """check config & values matches"""
408
+        for cmd in CONFIGS:
409
+            with self.subTest(cmd=cmd):
410
+                with unittest.mock.patch("os.popen", return_value=self.json()):
411
+                    conf = main([f"rspamd_{cmd}", "config"], self.environ)
412
+                    values = main([f"rspamd_{cmd}"], self.environ)
413
+
414
+                    keys = []
415
+                    for line in conf.split("\n"):
416
+                        prefix = line.split(" ")[0]
417
+                        if "." not in prefix:
418
+                            continue
419
+                        spl = prefix.split(".")
420
+                        if len(spl) < 2 or spl[1] != "label":
421
+                            continue
422
+                        keys.append(spl[0])
423
+
424
+                    for name in keys:
425
+                        self.assertIn(f"{name}.value ", values)
426
+
427
+    def test_results(self):
428
+        """munin values"""
429
+        for cmd, expt in EXPTS.items():
430
+            with self.subTest(cmd=cmd):
431
+                with unittest.mock.patch("os.popen", return_value=self.json()):
432
+                    ret = main([f"rspamd_{cmd}"], self.environ)
433
+                    self.assertEqual(set(ret.split("\n")), set(expt))
434
+
435
+    def test_suggest(self):
436
+        """munin suggest autoconf test"""
437
+        ret = main(["rspamd_", "suggest"], self.environ)
438
+        self.assertEqual(
439
+            set(ret.split("\n")), set(CONFIGS.keys()) | set(["#./rspamd_ suggest"])
440
+        )

+ 1
- 0
rspamd_.py View File

@@ -0,0 +1 @@
1
+rspamd_

+ 2
- 0
tests.sh View File

@@ -0,0 +1,2 @@
1
+#!/bin/sh
2
+/usr/bin/env python3 -m unittest rspamd_ $@

Loading…
Cancel
Save