mirror of https://github.com/fail2ban/fail2ban
ENH: Add fail2ban persistent data storage
parent
2c1199cce0
commit
bbadef847b
|
@ -47,3 +47,14 @@ socket = /var/run/fail2ban/fail2ban.sock
|
||||||
#
|
#
|
||||||
pidfile = /var/run/fail2ban/fail2ban.pid
|
pidfile = /var/run/fail2ban/fail2ban.pid
|
||||||
|
|
||||||
|
# Options: dbfile
|
||||||
|
# Notes.: Set the file for the fail2ban persistent data to be stored.
|
||||||
|
# A value of :memory: means database is only stored in memory, and
|
||||||
|
# data is lost once fail2ban is stops.
|
||||||
|
# Values: [ FILE ] :memory: Default: /var/lib/fail2ban/fail2ban.db
|
||||||
|
dbfile = /var/lib/fail2ban/fail2ban.db
|
||||||
|
|
||||||
|
# Options: dbpurgeage
|
||||||
|
# Notes.: Sets age at which bans should be purged from the database
|
||||||
|
# Values: [ SECONDS ] Default: 86400 (24hours)
|
||||||
|
dbpurgeage = 86400
|
||||||
|
|
|
@ -102,6 +102,12 @@ class Beautifier:
|
||||||
msg = msg + "DEBUG"
|
msg = msg + "DEBUG"
|
||||||
else:
|
else:
|
||||||
msg = msg + `response`
|
msg = msg + `response`
|
||||||
|
elif inC[1] == "dbfile":
|
||||||
|
msg = "Current database file is:\n"
|
||||||
|
msg = msg + "`- " + response
|
||||||
|
elif inC[1] == "dbpurgeage":
|
||||||
|
msg = "Current database purge age is:\n"
|
||||||
|
msg = msg + "`- %iseconds" % response
|
||||||
elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
|
elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
|
||||||
if len(response) == 0:
|
if len(response) == 0:
|
||||||
msg = "No file is currently monitored"
|
msg = "No file is currently monitored"
|
||||||
|
|
|
@ -45,7 +45,9 @@ class Fail2banReader(ConfigReader):
|
||||||
|
|
||||||
def getOptions(self):
|
def getOptions(self):
|
||||||
opts = [["int", "loglevel", 1],
|
opts = [["int", "loglevel", 1],
|
||||||
["string", "logtarget", "STDERR"]]
|
["string", "logtarget", "STDERR"],
|
||||||
|
["string", "dbfile", "/var/lib/fail2ban/fail2ban.db"],
|
||||||
|
["int", "dbpurgeage", "/var/lib/fail2ban/fail2ban.db"]]
|
||||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
|
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
|
||||||
|
|
||||||
def convert(self):
|
def convert(self):
|
||||||
|
@ -55,5 +57,9 @@ class Fail2banReader(ConfigReader):
|
||||||
stream.append(["set", "loglevel", self.__opts[opt]])
|
stream.append(["set", "loglevel", self.__opts[opt]])
|
||||||
elif opt == "logtarget":
|
elif opt == "logtarget":
|
||||||
stream.append(["set", "logtarget", self.__opts[opt]])
|
stream.append(["set", "logtarget", self.__opts[opt]])
|
||||||
|
elif opt == "dbfile":
|
||||||
|
stream.append(["set", "dbfile", self.__opts[opt]])
|
||||||
|
elif opt == "dbpurgeage":
|
||||||
|
stream.append(["set", "dbpurgeage", self.__opts[opt]])
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,11 @@ protocol = [
|
||||||
["get loglevel", "gets the logging level"],
|
["get loglevel", "gets the logging level"],
|
||||||
["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"],
|
["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"],
|
||||||
["get logtarget", "gets logging target"],
|
["get logtarget", "gets logging target"],
|
||||||
|
['', "DATABASE", ""],
|
||||||
|
["set dbfile <FILE>", "set the location of fail2ban persistent datastore"],
|
||||||
|
["get dbfile", "get the location of fail2ban persistent datastore"],
|
||||||
|
["set dbpurgeage <SECONDS>", "sets the max age in <SECONDS> that history of bans will be kept"],
|
||||||
|
["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"],
|
||||||
['', "JAIL CONTROL", ""],
|
['', "JAIL CONTROL", ""],
|
||||||
["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"],
|
["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"],
|
||||||
["start <JAIL>", "starts the jail <JAIL>"],
|
["start <JAIL>", "starts the jail <JAIL>"],
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||||
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||||
|
|
||||||
|
# This file is part of Fail2Ban.
|
||||||
|
#
|
||||||
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Fail2Ban is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Fail2Ban; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
__author__ = "Steven Hiscocks"
|
||||||
|
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
||||||
|
__license__ = "GPL"
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from threading import Lock
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import locale
|
||||||
|
|
||||||
|
from fail2ban.server.mytime import MyTime
|
||||||
|
from fail2ban.server.ticket import FailTicket
|
||||||
|
|
||||||
|
# Gets the instance of the logger.
|
||||||
|
logSys = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if sys.version_info >= (3,):
|
||||||
|
sqlite3.register_adapter(
|
||||||
|
dict,
|
||||||
|
lambda x: json.dumps(x, ensure_ascii=False).encode(
|
||||||
|
locale.getpreferredencoding(), 'replace'))
|
||||||
|
sqlite3.register_converter(
|
||||||
|
"JSON",
|
||||||
|
lambda x: json.loads(x.decode(
|
||||||
|
locale.getpreferredencoding(), 'replace')))
|
||||||
|
else:
|
||||||
|
sqlite3.register_adapter(dict, json.dumps)
|
||||||
|
sqlite3.register_converter("JSON", json.loads)
|
||||||
|
|
||||||
|
def lockandcommit():
|
||||||
|
def wrap(f):
|
||||||
|
def func(self, *args, **kw):
|
||||||
|
with self._lock: # Threading lock
|
||||||
|
with self._db: # Auto commit and rollback on exception
|
||||||
|
return f(self, self._db.cursor(), *args, **kw)
|
||||||
|
return func
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
class Fail2BanDb(object):
|
||||||
|
__version__ = 1
|
||||||
|
def __init__(self, filename, purgeAge=24*60*60):
|
||||||
|
self._lock = Lock()
|
||||||
|
try:
|
||||||
|
self._db = sqlite3.connect(
|
||||||
|
filename, check_same_thread=False,
|
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES)
|
||||||
|
self._dbFilename = filename
|
||||||
|
self._purgeAge = purgeAge
|
||||||
|
|
||||||
|
logSys.info(
|
||||||
|
"Connected to fail2ban persistent database '%s'", filename)
|
||||||
|
except sqlite3.OperationalError, e:
|
||||||
|
logSys.error(
|
||||||
|
"Error connecting to fail2ban persistent database '%s': %s",
|
||||||
|
filename, e.args[0])
|
||||||
|
raise
|
||||||
|
|
||||||
|
cur = self._db.cursor()
|
||||||
|
cur.execute("PRAGMA foreign_keys = ON;")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logSys.warning("New database created. Version '%i'",
|
||||||
|
self.createDb())
|
||||||
|
else:
|
||||||
|
version = cur.fetchone()[0]
|
||||||
|
if version < Fail2BanDb.__version__:
|
||||||
|
logSys.warning( "Database updated from '%i' to '%i'",
|
||||||
|
version, self.updateDb(version))
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
def getFilename(self):
|
||||||
|
return self._dbFilename
|
||||||
|
|
||||||
|
def getPurgeAge(self):
|
||||||
|
return self._purgeAge
|
||||||
|
|
||||||
|
def setPurgeAge(self, value):
|
||||||
|
self._purgeAge = int(value)
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def createDb(self, cur):
|
||||||
|
# Version info
|
||||||
|
cur.execute("CREATE TABLE fail2banDb(version INTEGER)")
|
||||||
|
cur.execute("INSERT INTO fail2banDb(version) VALUES(?)",
|
||||||
|
(Fail2BanDb.__version__, ))
|
||||||
|
|
||||||
|
# Jails
|
||||||
|
cur.execute("CREATE TABLE jails("
|
||||||
|
"name TEXT NOT NULL UNIQUE, "
|
||||||
|
"enabled INTEGER NOT NULL DEFAULT 1"
|
||||||
|
")")
|
||||||
|
cur.execute("CREATE INDEX jails_name ON jails(name)")
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
cur.execute("CREATE TABLE logs("
|
||||||
|
"jail TEXT NOT NULL, "
|
||||||
|
"path TEXT, "
|
||||||
|
"firstlinemd5 TEXT, "
|
||||||
|
#TODO: systemd journal features
|
||||||
|
#"journalmatch TEXT, "
|
||||||
|
#"journlcursor TEXT, "
|
||||||
|
"lastfilepos INTEGER DEFAULT 0, "
|
||||||
|
#"lastfiletime INTEGER DEFAULT 0, " # is this easily available
|
||||||
|
"FOREIGN KEY(jail) REFERENCES jails(name) ON DELETE CASCADE, "
|
||||||
|
"UNIQUE(jail, path)"
|
||||||
|
")")
|
||||||
|
cur.execute("CREATE INDEX logs_path ON logs(path)")
|
||||||
|
cur.execute("CREATE INDEX logs_jail_path ON logs(jail, path)")
|
||||||
|
|
||||||
|
# Bans
|
||||||
|
cur.execute("CREATE TABLE bans("
|
||||||
|
"jail TEXT NOT NULL, "
|
||||||
|
"ip TEXT, "
|
||||||
|
"timeofban INTEGER NOT NULL, "
|
||||||
|
"data JSON, "
|
||||||
|
"FOREIGN KEY(jail) REFERENCES jails(name) "
|
||||||
|
")")
|
||||||
|
cur.execute(
|
||||||
|
"CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban)")
|
||||||
|
cur.execute("CREATE INDEX bans_jail_ip ON bans(jail, ip)")
|
||||||
|
cur.execute("CREATE INDEX bans_ip ON bans(ip)")
|
||||||
|
|
||||||
|
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||||
|
return cur.fetchone()[0]
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def updateDb(self, cur, version):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Only single version of database exists...how did you get here??")
|
||||||
|
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||||
|
return cur.fetchone()[0]
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def addJail(self, cur, jail):
|
||||||
|
cur.execute(
|
||||||
|
"INSERT OR REPLACE INTO jails(name, enabled) VALUES(?, 1)",
|
||||||
|
(jail.getName(),))
|
||||||
|
|
||||||
|
def delJail(self, jail):
|
||||||
|
return self.delJailName(jail.getName())
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def delJailName(self, cur, name):
|
||||||
|
# Will be deleted by purge as appropriate
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE jails SET enabled=0 WHERE name=?", (name, ))
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def getJailNames(self, cur):
|
||||||
|
cur.execute("SELECT name FROM jails")
|
||||||
|
return set(row[0] for row in cur.fetchmany())
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def addLog(self, cur, jail, container):
|
||||||
|
lastLinePos = None
|
||||||
|
cur.execute(
|
||||||
|
"SELECT firstlinemd5, lastfilepos FROM logs "
|
||||||
|
"WHERE jail=? AND path=?",
|
||||||
|
(jail.getName(), container.getFileName()))
|
||||||
|
try:
|
||||||
|
firstLineMD5, lastLinePos = cur.fetchone()
|
||||||
|
except TypeError:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO logs(jail, path, firstlinemd5, lastfilepos) "
|
||||||
|
"VALUES(?, ?, ?, ?)",
|
||||||
|
(jail.getName(), container.getFileName(),
|
||||||
|
container.getHash(), container.getPos()))
|
||||||
|
else:
|
||||||
|
if container.getHash() != firstLineMD5:
|
||||||
|
self._updateLog(cur, jail, container)
|
||||||
|
lastLinePos = None
|
||||||
|
return lastLinePos
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def getLogPaths(self, cur, jail=None):
|
||||||
|
query = "SELECT path FROM logs"
|
||||||
|
queryArgs = []
|
||||||
|
if jail is not None:
|
||||||
|
query += " WHERE jail=?"
|
||||||
|
queryArgs.append(jail.getName())
|
||||||
|
cur.execute(query, queryArgs)
|
||||||
|
return set(row[0] for row in cur.fetchmany())
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def updateLog(self, cur, *args, **kwargs):
|
||||||
|
self._updateLog(cur, *args, **kwargs)
|
||||||
|
|
||||||
|
def _updateLog(self, cur, jail, container):
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE logs SET firstlinemd5=?, lastfilepos=? "
|
||||||
|
"WHERE jail=? AND path=?",
|
||||||
|
(container.getHash(), container.getPos(),
|
||||||
|
jail.getName(), container.getFileName()))
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def addBan(self, cur, jail, ticket):
|
||||||
|
#TODO: Implement data parts once arbitrary match keys completed
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
|
||||||
|
(jail.getName(), ticket.getIP(), ticket.getTime(),
|
||||||
|
{"matches": ticket.getMatches(),
|
||||||
|
"failures": ticket.getAttempt()}))
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def getBans(self, cur, jail=None, bantime=None):
|
||||||
|
query = "SELECT ip, timeofban, data FROM bans"
|
||||||
|
queryArgs = []
|
||||||
|
|
||||||
|
if jail is not None:
|
||||||
|
query += " WHERE jail=?"
|
||||||
|
queryArgs.append(jail.getName())
|
||||||
|
if bantime is not None:
|
||||||
|
query += " AND timeofban > ?"
|
||||||
|
queryArgs.append(MyTime.time() - bantime)
|
||||||
|
|
||||||
|
tickets = []
|
||||||
|
for ip, timeofban, data in cur.execute(query, queryArgs):
|
||||||
|
#TODO: Implement data parts once arbitrary match keys completed
|
||||||
|
tickets.append(FailTicket(ip, timeofban, data['matches']))
|
||||||
|
tickets[-1].setAttempt(data['failures'])
|
||||||
|
return tickets
|
||||||
|
|
||||||
|
@lockandcommit()
|
||||||
|
def purge(self, cur):
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM bans WHERE timeofban < ?",
|
||||||
|
(MyTime.time() - self._purgeAge, ))
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM jails WHERE enabled = 0 "
|
||||||
|
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)")
|
||||||
|
|
|
@ -527,6 +527,9 @@ class FileFilter(Filter):
|
||||||
logSys.error(path + " already exists")
|
logSys.error(path + " already exists")
|
||||||
else:
|
else:
|
||||||
container = FileContainer(path, self.getLogEncoding(), tail)
|
container = FileContainer(path, self.getLogEncoding(), tail)
|
||||||
|
lastpos = self.jail.getDatabase().addLog(self.jail, container)
|
||||||
|
if lastpos and not tail:
|
||||||
|
container.setPos(lastpos)
|
||||||
self.__logPath.append(container)
|
self.__logPath.append(container)
|
||||||
logSys.info("Added logfile = %s" % path)
|
logSys.info("Added logfile = %s" % path)
|
||||||
self._addLogPath(path) # backend specific
|
self._addLogPath(path) # backend specific
|
||||||
|
@ -546,6 +549,7 @@ class FileFilter(Filter):
|
||||||
for log in self.__logPath:
|
for log in self.__logPath:
|
||||||
if log.getFileName() == path:
|
if log.getFileName() == path:
|
||||||
self.__logPath.remove(log)
|
self.__logPath.remove(log)
|
||||||
|
self.jail.getDatabase().updateLog(self.jail, log)
|
||||||
logSys.info("Removed logfile = %s" % path)
|
logSys.info("Removed logfile = %s" % path)
|
||||||
self._delLogPath(path)
|
self._delLogPath(path)
|
||||||
return
|
return
|
||||||
|
@ -644,6 +648,7 @@ class FileFilter(Filter):
|
||||||
break
|
break
|
||||||
self.processLineAndAdd(line)
|
self.processLineAndAdd(line)
|
||||||
container.close()
|
container.close()
|
||||||
|
self.jail.getDatabase().updateLog(self.jail, container)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
|
@ -682,7 +687,7 @@ class FileContainer:
|
||||||
try:
|
try:
|
||||||
firstLine = handler.readline()
|
firstLine = handler.readline()
|
||||||
# Computes the MD5 of the first line.
|
# Computes the MD5 of the first line.
|
||||||
self.__hash = md5sum(firstLine).digest()
|
self.__hash = md5sum(firstLine).hexdigest()
|
||||||
# Start at the beginning of file if tail mode is off.
|
# Start at the beginning of file if tail mode is off.
|
||||||
if tail:
|
if tail:
|
||||||
handler.seek(0, 2)
|
handler.seek(0, 2)
|
||||||
|
@ -702,6 +707,15 @@ class FileContainer:
|
||||||
def getEncoding(self):
|
def getEncoding(self):
|
||||||
return self.__encoding
|
return self.__encoding
|
||||||
|
|
||||||
|
def getHash(self):
|
||||||
|
return self.__hash
|
||||||
|
|
||||||
|
def getPos(self):
|
||||||
|
return self.__pos
|
||||||
|
|
||||||
|
def setPos(self, value):
|
||||||
|
self.__pos = value
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
self.__handler = open(self.__filename, 'rb')
|
self.__handler = open(self.__filename, 'rb')
|
||||||
# Set the file descriptor to be FD_CLOEXEC
|
# Set the file descriptor to be FD_CLOEXEC
|
||||||
|
@ -717,7 +731,7 @@ class FileContainer:
|
||||||
return False
|
return False
|
||||||
firstLine = self.__handler.readline()
|
firstLine = self.__handler.readline()
|
||||||
# Computes the MD5 of the first line.
|
# Computes the MD5 of the first line.
|
||||||
myHash = md5sum(firstLine).digest()
|
myHash = md5sum(firstLine).hexdigest()
|
||||||
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % (
|
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % (
|
||||||
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
|
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
|
||||||
## self.__hash != myHash or self.__ino != stats.st_ino)
|
## self.__hash != myHash or self.__ino != stats.st_ino)
|
||||||
|
|
|
@ -37,7 +37,8 @@ class Jail:
|
||||||
# list had .index until 2.6
|
# list had .index until 2.6
|
||||||
_BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd']
|
_BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd']
|
||||||
|
|
||||||
def __init__(self, name, backend = "auto"):
|
def __init__(self, db, name, backend = "auto"):
|
||||||
|
self.__db = db
|
||||||
self.setName(name)
|
self.setName(name)
|
||||||
self.__queue = Queue.Queue()
|
self.__queue = Queue.Queue()
|
||||||
self.__filter = None
|
self.__filter = None
|
||||||
|
@ -118,6 +119,9 @@ class Jail:
|
||||||
def getName(self):
|
def getName(self):
|
||||||
return self.__name
|
return self.__name
|
||||||
|
|
||||||
|
def getDatabase(self):
|
||||||
|
return self.__db
|
||||||
|
|
||||||
def getFilter(self):
|
def getFilter(self):
|
||||||
return self.__filter
|
return self.__filter
|
||||||
|
|
||||||
|
@ -126,6 +130,7 @@ class Jail:
|
||||||
|
|
||||||
def putFailTicket(self, ticket):
|
def putFailTicket(self, ticket):
|
||||||
self.__queue.put(ticket)
|
self.__queue.put(ticket)
|
||||||
|
self.__db.addBan(self, ticket)
|
||||||
|
|
||||||
def getFailTicket(self):
|
def getFailTicket(self):
|
||||||
try:
|
try:
|
||||||
|
@ -136,6 +141,9 @@ class Jail:
|
||||||
def start(self):
|
def start(self):
|
||||||
self.__filter.start()
|
self.__filter.start()
|
||||||
self.__action.start()
|
self.__action.start()
|
||||||
|
# Restore any previous valid bans from the database
|
||||||
|
for ticket in self.__db.getBans(self, self.__action.getBanTime()):
|
||||||
|
self.__queue.put(ticket)
|
||||||
logSys.info("Jail '%s' started" % self.__name)
|
logSys.info("Jail '%s' started" % self.__name)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|
|
@ -50,13 +50,13 @@ class Jails:
|
||||||
# @param name The name of the jail
|
# @param name The name of the jail
|
||||||
# @param backend The backend to use
|
# @param backend The backend to use
|
||||||
|
|
||||||
def add(self, name, backend):
|
def add(self, db, name, backend):
|
||||||
try:
|
try:
|
||||||
self.__lock.acquire()
|
self.__lock.acquire()
|
||||||
if self.__jails.has_key(name):
|
if self.__jails.has_key(name):
|
||||||
raise DuplicateJailException(name)
|
raise DuplicateJailException(name)
|
||||||
else:
|
else:
|
||||||
self.__jails[name] = Jail(name, backend)
|
self.__jails[name] = Jail(db, name, backend)
|
||||||
finally:
|
finally:
|
||||||
self.__lock.release()
|
self.__lock.release()
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ from filter import FileFilter, JournalFilter
|
||||||
from transmitter import Transmitter
|
from transmitter import Transmitter
|
||||||
from asyncserver import AsyncServer
|
from asyncserver import AsyncServer
|
||||||
from asyncserver import AsyncServerException
|
from asyncserver import AsyncServerException
|
||||||
|
from database import Fail2BanDb
|
||||||
from fail2ban import version
|
from fail2ban import version
|
||||||
import logging, logging.handlers, sys, os, signal
|
import logging, logging.handlers, sys, os, signal
|
||||||
|
|
||||||
|
@ -50,6 +51,9 @@ class Server:
|
||||||
# Set logging level
|
# Set logging level
|
||||||
self.setLogLevel(3)
|
self.setLogLevel(3)
|
||||||
self.setLogTarget("STDOUT")
|
self.setLogTarget("STDOUT")
|
||||||
|
|
||||||
|
# Create database, initially in memory
|
||||||
|
self.setDatabase(":memory:")
|
||||||
|
|
||||||
def __sigTERMhandler(self, signum, frame):
|
def __sigTERMhandler(self, signum, frame):
|
||||||
logSys.debug("Caught signal %d. Exiting" % signum)
|
logSys.debug("Caught signal %d. Exiting" % signum)
|
||||||
|
@ -117,10 +121,13 @@ class Server:
|
||||||
|
|
||||||
|
|
||||||
def addJail(self, name, backend):
|
def addJail(self, name, backend):
|
||||||
self.__jails.add(name, backend)
|
self.__jails.add(self.__db, name, backend)
|
||||||
|
self.__db.addJail(self.__jails.get(name))
|
||||||
|
|
||||||
def delJail(self, name):
|
def delJail(self, name, dbDel=True):
|
||||||
self.__jails.remove(name)
|
self.__jails.remove(name)
|
||||||
|
if dbDel:
|
||||||
|
self.__db.delJailName(name)
|
||||||
|
|
||||||
def startJail(self, name):
|
def startJail(self, name):
|
||||||
try:
|
try:
|
||||||
|
@ -130,13 +137,13 @@ class Server:
|
||||||
finally:
|
finally:
|
||||||
self.__lock.release()
|
self.__lock.release()
|
||||||
|
|
||||||
def stopJail(self, name):
|
def stopJail(self, name, dbDel=True):
|
||||||
logSys.debug("Stopping jail %s" % name)
|
logSys.debug("Stopping jail %s" % name)
|
||||||
try:
|
try:
|
||||||
self.__lock.acquire()
|
self.__lock.acquire()
|
||||||
if self.isAlive(name):
|
if self.isAlive(name):
|
||||||
self.__jails.get(name).stop()
|
self.__jails.get(name).stop()
|
||||||
self.delJail(name)
|
self.delJail(name, dbDel=dbDel)
|
||||||
finally:
|
finally:
|
||||||
self.__lock.release()
|
self.__lock.release()
|
||||||
|
|
||||||
|
@ -145,7 +152,7 @@ class Server:
|
||||||
try:
|
try:
|
||||||
self.__lock.acquire()
|
self.__lock.acquire()
|
||||||
for jail in self.__jails.getAll():
|
for jail in self.__jails.getAll():
|
||||||
self.stopJail(jail)
|
self.stopJail(jail, dbDel=False)
|
||||||
finally:
|
finally:
|
||||||
self.__lock.release()
|
self.__lock.release()
|
||||||
|
|
||||||
|
@ -460,6 +467,16 @@ class Server:
|
||||||
finally:
|
finally:
|
||||||
self.__loggingLock.release()
|
self.__loggingLock.release()
|
||||||
|
|
||||||
|
def setDatabase(self, filename):
|
||||||
|
if self.__jails.size() == 0:
|
||||||
|
self.__db = Fail2BanDb(filename)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot change database when there are jails present")
|
||||||
|
|
||||||
|
def getDatabase(self):
|
||||||
|
return self.__db
|
||||||
|
|
||||||
def __createDaemon(self): # pragma: no cover
|
def __createDaemon(self): # pragma: no cover
|
||||||
""" Detach a process from the controlling terminal and run it in the
|
""" Detach a process from the controlling terminal and run it in the
|
||||||
background as a daemon.
|
background as a daemon.
|
||||||
|
|
|
@ -113,6 +113,13 @@ class Transmitter:
|
||||||
return self.__server.getLogTarget()
|
return self.__server.getLogTarget()
|
||||||
else:
|
else:
|
||||||
raise Exception("Failed to change log target")
|
raise Exception("Failed to change log target")
|
||||||
|
#Database
|
||||||
|
elif name == "dbfile":
|
||||||
|
self.__server.setDatabase(command[1])
|
||||||
|
return self.__server.getDatabase().getFilename()
|
||||||
|
elif name == "dbpurgeage":
|
||||||
|
self.__server.getDatabase().setPurgeAge(command[1])
|
||||||
|
return self.__server.getDatabase().getPurgeAge()
|
||||||
# Jail
|
# Jail
|
||||||
elif command[1] == "idle":
|
elif command[1] == "idle":
|
||||||
if command[2] == "on":
|
if command[2] == "on":
|
||||||
|
@ -257,6 +264,11 @@ class Transmitter:
|
||||||
return self.__server.getLogLevel()
|
return self.__server.getLogLevel()
|
||||||
elif name == "logtarget":
|
elif name == "logtarget":
|
||||||
return self.__server.getLogTarget()
|
return self.__server.getLogTarget()
|
||||||
|
#Database
|
||||||
|
elif name == "dbfile":
|
||||||
|
return self.__server.getDatabase().getFilename()
|
||||||
|
elif name == "dbpurgeage":
|
||||||
|
return self.__server.getDatabase().getPurgeAge()
|
||||||
# Filter
|
# Filter
|
||||||
elif command[1] == "logpath":
|
elif command[1] == "logpath":
|
||||||
return self.__server.getLogPath(name)
|
return self.__server.getLogPath(name)
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||||
|
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||||
|
|
||||||
|
# This file is part of Fail2Ban.
|
||||||
|
#
|
||||||
|
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Fail2Ban is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Fail2Ban; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
# Fail2Ban developers
|
||||||
|
|
||||||
|
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
||||||
|
__license__ = "GPL"
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from fail2ban.server.database import Fail2BanDb
|
||||||
|
from fail2ban.server.filter import FileContainer
|
||||||
|
from fail2ban.server.mytime import MyTime
|
||||||
|
from fail2ban.server.ticket import FailTicket
|
||||||
|
from fail2ban.tests.dummyjail import DummyJail
|
||||||
|
|
||||||
|
class DatabaseTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Call before every test case."""
|
||||||
|
_, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_")
|
||||||
|
self.db = Fail2BanDb(self.dbFilename)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Call after every test case."""
|
||||||
|
# Cleanup
|
||||||
|
os.remove(self.dbFilename)
|
||||||
|
|
||||||
|
def getFilename(self):
|
||||||
|
self.assertEqual(self.dbFilename, self.db.getFilename())
|
||||||
|
|
||||||
|
def testCreateInvalidPath(self):
|
||||||
|
self.assertRaises(
|
||||||
|
sqlite3.OperationalError,
|
||||||
|
Fail2BanDb,
|
||||||
|
"/this/path/should/not/exist")
|
||||||
|
|
||||||
|
def testCreateAndReconnect(self):
|
||||||
|
self.testAddJail()
|
||||||
|
# Reconnect...
|
||||||
|
self.db = Fail2BanDb(self.dbFilename)
|
||||||
|
# and check jail of same name still present
|
||||||
|
self.assertTrue(
|
||||||
|
self.jail.getName() in self.db.getJailNames(),
|
||||||
|
"Jail not retained in Db after disconnect reconnect.")
|
||||||
|
|
||||||
|
def testUpdateDb(self):
|
||||||
|
# TODO: Currently only single version exists
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testAddJail(self):
|
||||||
|
self.jail = DummyJail()
|
||||||
|
self.db.addJail(self.jail)
|
||||||
|
self.assertTrue(
|
||||||
|
self.jail.getName() in self.db.getJailNames(),
|
||||||
|
"Jail not added to database")
|
||||||
|
|
||||||
|
def testAddLog(self):
|
||||||
|
self.testAddJail() # Jail required
|
||||||
|
|
||||||
|
_, filename = tempfile.mkstemp(".log", "Fail2BanDb_")
|
||||||
|
self.fileContainer = FileContainer(filename, "utf-8")
|
||||||
|
|
||||||
|
self.db.addLog(self.jail, self.fileContainer)
|
||||||
|
|
||||||
|
self.assertTrue(filename in self.db.getLogPaths(self.jail))
|
||||||
|
|
||||||
|
def testUpdateLog(self):
|
||||||
|
self.testAddLog() # Add log file
|
||||||
|
|
||||||
|
# Write some text
|
||||||
|
filename = self.fileContainer.getFileName()
|
||||||
|
file_ = open(filename, "w")
|
||||||
|
file_.write("Some text to write which will change md5sum\n")
|
||||||
|
file_.close()
|
||||||
|
self.fileContainer.open()
|
||||||
|
self.fileContainer.readline()
|
||||||
|
self.fileContainer.close()
|
||||||
|
|
||||||
|
# Capture position which should be after line just written
|
||||||
|
lastPos = self.fileContainer.getPos()
|
||||||
|
self.assertTrue(lastPos > 0)
|
||||||
|
self.db.updateLog(self.jail, self.fileContainer)
|
||||||
|
|
||||||
|
# New FileContainer for file
|
||||||
|
self.fileContainer = FileContainer(filename, "utf-8")
|
||||||
|
self.assertEqual(self.fileContainer.getPos(), 0)
|
||||||
|
|
||||||
|
# Database should return previous position in file
|
||||||
|
self.assertEqual(
|
||||||
|
self.db.addLog(self.jail, self.fileContainer), lastPos)
|
||||||
|
|
||||||
|
# Change md5sum
|
||||||
|
file_ = open(filename, "w") # Truncate
|
||||||
|
file_.write("Some different text to change md5sum\n")
|
||||||
|
file_.close()
|
||||||
|
|
||||||
|
self.fileContainer = FileContainer(filename, "utf-8")
|
||||||
|
self.assertEqual(self.fileContainer.getPos(), 0)
|
||||||
|
|
||||||
|
# Database should be aware of md5sum change, such doesn't return
|
||||||
|
# last position in file
|
||||||
|
self.assertEqual(
|
||||||
|
self.db.addLog(self.jail, self.fileContainer), None)
|
||||||
|
|
||||||
|
def testAddBan(self):
|
||||||
|
self.testAddJail()
|
||||||
|
ticket = FailTicket("127.0.0.1", 0, [])
|
||||||
|
self.db.addBan(self.jail, ticket)
|
||||||
|
|
||||||
|
self.assertEquals(len(self.db.getBans(self.jail)), 1)
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(self.db.getBans(self.jail)[0], FailTicket))
|
||||||
|
|
||||||
|
def testPurge(self):
|
||||||
|
self.testAddJail() # Add jail
|
||||||
|
|
||||||
|
self.db.purge() # Jail enabled by default so shouldn't be purged
|
||||||
|
self.assertEqual(len(self.db.getJailNames()), 1)
|
||||||
|
|
||||||
|
self.db.delJail(self.jail)
|
||||||
|
self.db.purge() # Should remove jail
|
||||||
|
self.assertEqual(len(self.db.getJailNames()), 0)
|
||||||
|
|
||||||
|
self.testAddBan()
|
||||||
|
self.db.delJail(self.jail)
|
||||||
|
self.db.purge() # Purge should remove all bans
|
||||||
|
self.assertEqual(len(self.db.getJailNames()), 0)
|
||||||
|
self.assertEqual(len(self.db.getBans(self.jail)), 0)
|
||||||
|
|
||||||
|
# Should leave jail
|
||||||
|
self.testAddJail()
|
||||||
|
self.db.addBan(self.jail, FailTicket("127.0.0.1", MyTime.time(), []))
|
||||||
|
self.db.delJail(self.jail)
|
||||||
|
self.db.purge() # Should leave jail as ban present
|
||||||
|
self.assertEqual(len(self.db.getJailNames()), 1)
|
||||||
|
self.assertEqual(len(self.db.getBans(self.jail)), 1)
|
|
@ -23,12 +23,17 @@ __copyright__ = "Copyright (c) 2012 Yaroslav Halchenko"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
|
from fail2ban.server.database import Fail2BanDb
|
||||||
|
|
||||||
class DummyJail(object):
|
class DummyJail(object):
|
||||||
"""A simple 'jail' to suck in all the tickets generated by Filter's
|
"""A simple 'jail' to suck in all the tickets generated by Filter's
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.queue = []
|
self.queue = []
|
||||||
|
self.__db = Fail2BanDb(":memory:")
|
||||||
|
self.__db.addJail(self)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
try:
|
try:
|
||||||
|
@ -54,6 +59,15 @@ class DummyJail(object):
|
||||||
finally:
|
finally:
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
def setIdle(self, value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getIdle(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def getName(self):
|
def getName(self):
|
||||||
return "DummyJail #%s with %d tickets" % (id(self), len(self))
|
return "DummyJail #%s with %d tickets" % (id(self), len(self))
|
||||||
|
|
||||||
|
def getDatabase(self):
|
||||||
|
return self.__db
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ from fail2ban.server.filter import FileFilter, DNSUtils
|
||||||
from fail2ban.server.failmanager import FailManager
|
from fail2ban.server.failmanager import FailManager
|
||||||
from fail2ban.server.failmanager import FailManagerEmpty
|
from fail2ban.server.failmanager import FailManagerEmpty
|
||||||
from fail2ban.server.mytime import MyTime
|
from fail2ban.server.mytime import MyTime
|
||||||
|
from fail2ban.server.database import Fail2BanDb
|
||||||
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime
|
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime
|
||||||
|
|
||||||
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
|
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
|
||||||
|
@ -228,7 +229,7 @@ class LogFile(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Call before every test case."""
|
"""Call before every test case."""
|
||||||
self.filter = FilterPoll(None)
|
self.filter = FilterPoll(DummyJail())
|
||||||
self.filter.addLogPath(LogFile.FILENAME)
|
self.filter.addLogPath(LogFile.FILENAME)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -251,7 +252,7 @@ class LogFileMonitor(unittest.TestCase):
|
||||||
self.filter = self.name = 'NA'
|
self.filter = self.name = 'NA'
|
||||||
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
|
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
|
||||||
self.file = open(self.name, 'a')
|
self.file = open(self.name, 'a')
|
||||||
self.filter = FilterPoll(None)
|
self.filter = FilterPoll(DummyJail())
|
||||||
self.filter.addLogPath(self.name)
|
self.filter.addLogPath(self.name)
|
||||||
self.filter.setActive(True)
|
self.filter.setActive(True)
|
||||||
self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>")
|
self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>")
|
||||||
|
@ -564,7 +565,7 @@ def get_monitor_failures_testcase(Filter_):
|
||||||
# tail written before, so let's not copy anything yet
|
# tail written before, so let's not copy anything yet
|
||||||
#_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100)
|
#_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100)
|
||||||
# we should detect the failures
|
# we should detect the failures
|
||||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=6) # was needed if we write twice above
|
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=3) # was needed if we write twice above
|
||||||
|
|
||||||
# now copy and get even more
|
# now copy and get even more
|
||||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100)
|
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, n=100)
|
||||||
|
@ -715,7 +716,8 @@ class GetFailures(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Call before every test case."""
|
"""Call before every test case."""
|
||||||
setUpMyTime()
|
setUpMyTime()
|
||||||
self.filter = FileFilter(None)
|
self.jail = DummyJail()
|
||||||
|
self.filter = FileFilter(self.jail)
|
||||||
self.filter.setActive(True)
|
self.filter.setActive(True)
|
||||||
# TODO Test this
|
# TODO Test this
|
||||||
#self.filter.setTimeRegex("\S{3}\s{1,2}\d{1,2} \d{2}:\d{2}:\d{2}")
|
#self.filter.setTimeRegex("\S{3}\s{1,2}\d{1,2} \d{2}:\d{2}:\d{2}")
|
||||||
|
@ -802,7 +804,8 @@ class GetFailures(unittest.TestCase):
|
||||||
for useDns, output in (('yes', output_yes),
|
for useDns, output in (('yes', output_yes),
|
||||||
('no', output_no),
|
('no', output_no),
|
||||||
('warn', output_yes)):
|
('warn', output_yes)):
|
||||||
filter_ = FileFilter(None, useDns=useDns)
|
jail = DummyJail()
|
||||||
|
filter_ = FileFilter(jail, useDns=useDns)
|
||||||
filter_.setActive(True)
|
filter_.setActive(True)
|
||||||
filter_.failManager.setMaxRetry(1) # we might have just few failures
|
filter_.failManager.setMaxRetry(1) # we might have just few failures
|
||||||
|
|
||||||
|
@ -916,5 +919,6 @@ class JailTests(unittest.TestCase):
|
||||||
|
|
||||||
def testSetBackend_gh83(self):
|
def testSetBackend_gh83(self):
|
||||||
# smoke test
|
# smoke test
|
||||||
jail = Jail('test', backend='polling') # Must not fail to initiate
|
# Must not fail to initiate
|
||||||
|
jail = Jail(Fail2BanDb(":memory:"), 'test', backend='polling')
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import unittest, socket, time, tempfile, os, locale, sys
|
||||||
|
|
||||||
from fail2ban.server.server import Server
|
from fail2ban.server.server import Server
|
||||||
from fail2ban.server.jail import Jail
|
from fail2ban.server.jail import Jail
|
||||||
|
from fail2ban.server.database import Fail2BanDb
|
||||||
from fail2ban.exceptions import UnknownJailException
|
from fail2ban.exceptions import UnknownJailException
|
||||||
try:
|
try:
|
||||||
from fail2ban.server import filtersystemd
|
from fail2ban.server import filtersystemd
|
||||||
|
@ -168,6 +169,15 @@ class Transmitter(TransmitterBase):
|
||||||
# Approx 1 second delay
|
# Approx 1 second delay
|
||||||
self.assertAlmostEqual(t1 - t0, 1, places=2)
|
self.assertAlmostEqual(t1 - t0, 1, places=2)
|
||||||
|
|
||||||
|
def testDatabase(self):
|
||||||
|
_, tmpFilename = tempfile.mkstemp(".db", "Fail2Ban_")
|
||||||
|
# Jails present, cant change database
|
||||||
|
self.setGetTestNOK("dbfile", tmpFilename)
|
||||||
|
self.server.delJail(self.jailName)
|
||||||
|
self.setGetTest("dbfile", tmpFilename)
|
||||||
|
self.setGetTest("dbpurgeage", 600)
|
||||||
|
self.setGetTestNOK("dbpurgeage", "LIZARD")
|
||||||
|
|
||||||
def testAddJail(self):
|
def testAddJail(self):
|
||||||
jail2 = "TestJail2"
|
jail2 = "TestJail2"
|
||||||
jail3 = "TestJail3"
|
jail3 = "TestJail3"
|
||||||
|
@ -641,5 +651,5 @@ class JailTests(unittest.TestCase):
|
||||||
def testLongName(self):
|
def testLongName(self):
|
||||||
# Just a smoke test for now
|
# Just a smoke test for now
|
||||||
longname = "veryveryverylongname"
|
longname = "veryveryverylongname"
|
||||||
jail = Jail(longname)
|
jail = Jail(Fail2BanDb(":memory:"), longname)
|
||||||
self.assertEqual(jail.getName(), longname)
|
self.assertEqual(jail.getName(), longname)
|
||||||
|
|
|
@ -146,6 +146,7 @@ def gatherTests(regexps=None, no_network=False):
|
||||||
from fail2ban.tests import actiontestcase
|
from fail2ban.tests import actiontestcase
|
||||||
from fail2ban.tests import sockettestcase
|
from fail2ban.tests import sockettestcase
|
||||||
from fail2ban.tests import misctestcase
|
from fail2ban.tests import misctestcase
|
||||||
|
from fail2ban.tests import databasetestcase
|
||||||
if json:
|
if json:
|
||||||
from fail2ban.tests import samplestestcase
|
from fail2ban.tests import samplestestcase
|
||||||
|
|
||||||
|
@ -184,6 +185,8 @@ def gatherTests(regexps=None, no_network=False):
|
||||||
tests.addTest(unittest.makeSuite(misctestcase.HelpersTest))
|
tests.addTest(unittest.makeSuite(misctestcase.HelpersTest))
|
||||||
tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
|
tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
|
||||||
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
|
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
|
||||||
|
# Database
|
||||||
|
tests.addTest(unittest.makeSuite(databasetestcase.DatabaseTest))
|
||||||
|
|
||||||
# Filter
|
# Filter
|
||||||
if not no_network:
|
if not no_network:
|
||||||
|
|
Loading…
Reference in New Issue