implemented `fail2ban-client stats` (or alias `fail2ban-client statistic[s]`) for tabulated output of fail2ban stats

amend to #2975
pull/3641/merge
sebres 2024-04-24 18:49:59 +02:00
parent bdba42edd9
commit 3ca3646472
10 changed files with 97 additions and 9 deletions

View File

@ -103,6 +103,46 @@ class Beautifier:
msg.append(" %s Jail: %s" % (prefix1, n))
jail_stat(j, " " if i == len(jstat) else " | ")
msg = "\n".join(msg)
elif inC[0:1] == ['stats'] or inC[0:1] == ['statistics']:
def _statstable(response):
tophead = ["Jail", "Backend", "Filter", "Actions"]
headers = ["", "", "cur", "tot", "cur", "tot"]
minlens = [8, 8, 3, 3, 3, 3]
ralign = [0, 0, 1, 1, 1, 1]
rows = [[n, r[0], *r[1], *r[2]] for n, r in response.items()]
lens = []
for i in range(len(rows[0])):
col = (len(str(s[i])) for s in rows)
lens.append(max(minlens[i], max(col)))
rfmt = []
hfmt = []
for i in range(len(rows[0])):
f = "%%%ds" if ralign[i] else "%%-%ds"
rfmt.append(f % lens[i])
hfmt.append(f % lens[i])
rfmt = [rfmt[0], rfmt[1], "%s \u2502 %s" % (rfmt[2], rfmt[3]), "%s \u2502 %s" % (rfmt[4], rfmt[5])]
hfmt = [hfmt[0], hfmt[1], "%s \u2502 %s" % (hfmt[2], hfmt[3]), "%s \u2502 %s" % (hfmt[4], hfmt[5])]
tlens = [lens[0], lens[1], 3 + lens[2] + lens[3], 3 + lens[4] + lens[5]]
tfmt = [hfmt[0], hfmt[1], "%%-%ds" % (tlens[2],), "%%-%ds" % (tlens[3],)]
tsep = tfmt[0:2]
rfmt = " \u2551 ".join(rfmt)
hfmt = " \u2551 ".join(hfmt)
tfmt = " \u2551 ".join(tfmt)
tsep = " \u2551 ".join(tsep)
separator = ((tsep % tuple(tophead[0:2])) + " \u255F\u2500" +
("\u2500\u256B\u2500".join(['\u2500' * n for n in tlens[2:]])) + '\u2500')
ret = []
ret.append(tfmt % tuple(["", ""]+tophead[2:]))
ret.append(separator)
ret.append(hfmt % tuple(headers))
separator = "\u2550\u256C\u2550".join(['\u2550' * n for n in tlens]) + '\u2550'
ret.append(separator)
for row in rows:
ret.append(rfmt % tuple(row))
separator = "\u2550\u2569\u2550".join(['\u2550' * n for n in tlens]) + '\u2550'
ret.append(separator)
return ret
msg = "\n".join(_statstable(response))
elif len(inC) < 2:
pass # to few cmd args for below
elif inC[1] == "syslogsocket":

View File

@ -59,6 +59,7 @@ protocol = [
["banned <IP> ... <IP>]", "return list(s) of jails where given IP(s) are banned"],
["status", "gets the current status of the server"],
["status --all [FLAVOR]", "gets the current status of all jails, with optional flavor or extended info"],
["stat[istic]s", "gets the current statistics of all jails as table"],
["ping", "tests if the server is alive"],
["echo", "for internal usage, returns back and outputs a given string"],
["help", "return this output"],

View File

@ -721,9 +721,11 @@ class Actions(JailThread, Mapping):
"""Status of current and total ban counts and current banned IP list.
"""
# TODO: Allow this list to be printed as 'status' output
supported_flavors = ["short", "basic", "cymru"]
supported_flavors = ["short", "basic", "stats", "cymru"]
if flavor is None or flavor not in supported_flavors:
logSys.warning("Unsupported extended jail status flavor %r. Supported: %s" % (flavor, supported_flavors))
if flavor == "stats":
return (self.banManager.size(), self.banManager.getBanTotal())
# Always print this information (basic)
if flavor != "short":
banned = self.banManager.getBanList()

View File

@ -978,6 +978,8 @@ class Filter(JailThread):
def status(self, flavor="basic"):
"""Status of failures detected by filter.
"""
if flavor == "stats":
return (self.failManager.size(), self.failManager.getFailTotal())
ret = [("Currently failed", self.failManager.size()),
("Total failed", self.failManager.getFailTotal())]
return ret
@ -1255,6 +1257,8 @@ class FileFilter(Filter):
"""Status of Filter plus files being monitored.
"""
ret = super(FileFilter, self).status(flavor=flavor)
if flavor == "stats":
return ret
path = list(self.__logs.keys())
ret.append(("File list", path))
return ret

View File

@ -429,6 +429,8 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
def status(self, flavor="basic"):
ret = super(FilterSystemd, self).status(flavor=flavor)
if flavor == "stats":
return ret
ret.append(("Journal matches",
[" + ".join(" ".join(match) for match in self.__matches)]))
return ret

View File

@ -81,8 +81,9 @@ class Jail(object):
# Extra parameters for increase ban time
self._banExtra = {};
logSys.info("Creating new jail '%s'" % self.name)
self._realBackend = None
if backend is not None:
self._setBackend(backend)
self._realBackend = self._setBackend(backend)
self.backend = backend
def __repr__(self):
@ -113,7 +114,7 @@ class Jail(object):
else:
logSys.info("Initiated %r backend" % b)
self.__actions = Actions(self)
return # we are done
return b # we are done
except ImportError as e: # pragma: no cover
# Log debug if auto, but error if specific
logSys.log(
@ -185,10 +186,15 @@ class Jail(object):
def status(self, flavor="basic"):
"""The status of the jail.
"""
fstat = self.filter.status(flavor=flavor)
astat = self.actions.status(flavor=flavor)
if flavor == "stats":
backend = type(self.filter).__name__.replace('Filter', '').lower()
return [self._realBackend or self.backend, fstat, astat]
return [
("Filter", self.filter.status(flavor=flavor)),
("Actions", self.actions.status(flavor=flavor)),
]
("Filter", fstat),
("Actions", astat),
]
@property
def hasFailTickets(self):

View File

@ -608,13 +608,18 @@ class Server:
try:
self.__lock.acquire()
jails = sorted(self.__jails.items())
jailList = [n for n, j in jails]
ret = [("Number of jail", len(jailList)),
("Jail list", ", ".join(jailList))]
if flavor != "stats":
jailList = [n for n, j in jails]
ret = [
("Number of jail", len(jailList)),
("Jail list", ", ".join(jailList))
]
if name == '--all':
jstat = dict(jails)
for n, j in jails:
jstat[n] = j.status(flavor=flavor)
if flavor == "stats":
return jstat
ret.append(jstat)
return ret
finally:

View File

@ -144,6 +144,8 @@ class Transmitter:
return self.__commandGet(command[1:])
elif name == "status":
return self.status(command[1:])
elif name in ("stats", "statistic", "statistics"):
return self.__server.status("--all", "stats")
elif name == "version":
return version.version
elif name == "config-error":

View File

@ -168,6 +168,27 @@ class BeautifierTest(unittest.TestCase):
)
self.assertEqual(self.b.beautify(response), output)
def testStatusStats(self):
self.b.setInputCmd(["stats"])
response = {
"ssh": ["systemd", (3, 6), (12, 24)],
"exim4": ["pyinotify", (6, 12), (20, 20)],
"jail-with-long-name": ["polling", (0, 0), (0, 0)]
}
output = (""
+ " ? ? Filter ? Actions \n"
+ "Jail ? Backend ????????????????????????\n"
+ " ? ? cur ? tot ? cur ? tot\n"
+ "????????????????????????????????????????????????????????\n"
+ "ssh ? systemd ? 3 ? 6 ? 12 ? 24\n"
+ "exim4 ? pyinotify ? 6 ? 12 ? 20 ? 20\n"
+ "jail-with-long-name ? polling ? 0 ? 0 ? 0 ? 0\n"
+ "????????????????????????????????????????????????????????"
)
response = self.b.beautify(response).encode('ascii', 'replace').decode('ascii')
self.assertEqual(response, output)
def testFlushLogs(self):
self.b.setInputCmd(["flushlogs"])
self.assertEqual(self.b.beautify("rolled over"), "logs: rolled over")

View File

@ -645,6 +645,11 @@ class Transmitter(TransmitterBase):
(0, [('Number of jail', len(jails)), ('Jail list', ", ".join(jails)),
{"TestJail1": self._JAIL_STATUS, "TestJail2": self._JAIL_STATUS}
]))
self.assertEqual(self.transm.proceed(["stats"]),
(0, {
"TestJail1": [FAST_BACKEND, (0, 0), (0, 0)],
"TestJail2": [FAST_BACKEND, (0, 0), (0, 0)]
}))
def testJailStatus(self):
self.assertEqual(self.transm.proceed(["status", self.jailName]),