No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rspamd_ 12KB

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