Merge branch '_tent/cache-config-read' of https://github.com/sebres/fail2ban

* '_tent/cache-config-read' of https://github.com/sebres/fail2ban:
  code review, change log entries added;
  reset share/cache storage (if we use 'reload' in client with interactive mode)
  normalize tabs/spaces in docstrings;
  cache-config-read-v2 merged; logging normalized, set log level for loading (read or use shared) file(s) to INFO; prevent to read some files twice by read inside "_getIncludes" and by "read" self (occurred by only one file);
  code review; more stable config sharing, configurator always shares it config readers now;
  code review: use the same code (corresponding test cases - with sharing on and without it);
  rewritten caching resp. sharing of ConfigReader and SafeConfigParserWithIncludes (v.2, first and second level cache, without fingerprinting etc.);
  code review
  ConfigReader/ConfigWrapper renamed as suggested from @yarikoptic; + code clarifying (suggested also);
  Partially merge remote-tracking from 'sebres:cache-config-read-820': test cases extended, configurator.py adapted for test case.
  ENH: keep spitting out logging to the screen in LogCaptureTestCases if HEAVYDEBUG
  test case for check the read of config files will be cached;
  more precise by test
  ConfigWrapper class introduced: sharing of the same ConfigReader object between JailsReader and JailReader (don't read jail config each jail); sharing of the same DefinitionInitConfigReader (ActionReader, FilterReader) between all jails using that; cache of read a config files was optimized; test case extended for all types of config readers;
  config cache optimized - prevent to read the same config file inside different resources multiple times; test case: read jail file only once;
  test case for check the read of config files will be cached;
  caching of read config files, to make start of fail2ban faster, see issue #820
pull/815/head
Yaroslav Halchenko 2014-10-23 14:28:33 -04:00
commit 78e1a13fad
11 changed files with 334 additions and 62 deletions

View File

@ -16,6 +16,8 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
provides defaults for the chain, port, protocol and name tags provides defaults for the chain, port, protocol and name tags
- Fixes: - Fixes:
* start of file2ban aborted (on slow hosts, systemd considers the server has been
timed out and kills him), see gh-824
* UTF-8 fixes in pure-ftp thanks to Johannes Weberhofer. Closes gh-806. * UTF-8 fixes in pure-ftp thanks to Johannes Weberhofer. Closes gh-806.
* systemd backend error on bad utf-8 in python3 * systemd backend error on bad utf-8 in python3
* badips.py action error when logging HTTP error raised with badips request * badips.py action error when logging HTTP error raised with badips request
@ -64,6 +66,10 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger
- Added Cloudflare API action - Added Cloudflare API action
- Enhancements - Enhancements
* Start performance of fail2ban-client (and tests) increased, start time
and cpu usage rapidly reduced. Introduced a shared storage logic, to bypass
reading lots of config files (see gh-824).
Thanks to Joost Molenaar for good catch (reported gh-820).
* Fail2ban-regex - add print-all-matched option. Closes gh-652 * Fail2ban-regex - add print-all-matched option. Closes gh-652
* Suppress fail2ban-client warnings for non-critical config options * Suppress fail2ban-client warnings for non-critical config options
* Match non "Bye Bye" disconnect messages for sshd locked account regex * Match non "Bye Bye" disconnect messages for sshd locked account regex

View File

@ -409,6 +409,7 @@ class Fail2banClient:
# TODO: get away from stew of return codes and exception # TODO: get away from stew of return codes and exception
# handling -- handle via exceptions # handling -- handle via exceptions
try: try:
self.__configurator.Reload()
self.__configurator.readAll() self.__configurator.readAll()
ret = self.__configurator.getOptions(jail) ret = self.__configurator.getOptions(jail)
self.__configurator.convertToProtocol() self.__configurator.convertToProtocol()

View File

@ -26,7 +26,7 @@ __license__ = "GPL"
import os import os
from .configreader import ConfigReader, DefinitionInitConfigReader from .configreader import DefinitionInitConfigReader
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
@ -47,15 +47,19 @@ class ActionReader(DefinitionInitConfigReader):
DefinitionInitConfigReader.__init__( DefinitionInitConfigReader.__init__(
self, file_, jailName, initOpts, **kwargs) self, file_, jailName, initOpts, **kwargs)
def setFile(self, fileName):
self.__file = fileName
DefinitionInitConfigReader.setFile(self, os.path.join("action.d", fileName))
def getFile(self):
return self.__file
def setName(self, name): def setName(self, name):
self._name = name self._name = name
def getName(self): def getName(self):
return self._name return self._name
def read(self):
return ConfigReader.read(self, os.path.join("action.d", self._file))
def convert(self): def convert(self):
head = ["set", self._jailName] head = ["set", self._jailName]
stream = list() stream = list()

View File

@ -62,6 +62,7 @@ else: # pragma: no cover
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
logLevel = 7
__all__ = ['SafeConfigParserWithIncludes'] __all__ = ['SafeConfigParserWithIncludes']
@ -98,30 +99,73 @@ after = 1.conf
if sys.version_info >= (3,2): if sys.version_info >= (3,2):
# overload constructor only for fancy new Python3's # overload constructor only for fancy new Python3's
def __init__(self, *args, **kwargs): def __init__(self, share_config=None, *args, **kwargs):
kwargs = kwargs.copy() kwargs = kwargs.copy()
kwargs['interpolation'] = BasicInterpolationWithName() kwargs['interpolation'] = BasicInterpolationWithName()
kwargs['inline_comment_prefixes'] = ";" kwargs['inline_comment_prefixes'] = ";"
super(SafeConfigParserWithIncludes, self).__init__( super(SafeConfigParserWithIncludes, self).__init__(
*args, **kwargs) *args, **kwargs)
self._cfg_share = share_config
#@staticmethod else:
def getIncludes(resource, seen = []): def __init__(self, share_config=None, *args, **kwargs):
SafeConfigParser.__init__(self, *args, **kwargs)
self._cfg_share = share_config
@property
def share_config(self):
return self._cfg_share
def _getSharedSCPWI(self, filename):
SCPWI = SafeConfigParserWithIncludes
# read single one, add to return list, use sharing if possible:
if self._cfg_share:
# cache/share each file as include (ex: filter.d/common could be included in each filter config):
hashv = 'inc:'+(filename if not isinstance(filename, list) else '\x01'.join(filename))
cfg, i = self._cfg_share.get(hashv, (None, None))
if cfg is None:
cfg = SCPWI(share_config=self._cfg_share)
i = cfg.read(filename, get_includes=False)
self._cfg_share[hashv] = (cfg, i)
elif logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Shared file: %s", filename)
else:
# don't have sharing:
cfg = SCPWI()
i = cfg.read(filename, get_includes=False)
return (cfg, i)
def _getIncludes(self, filenames, seen=[]):
if not isinstance(filenames, list):
filenames = [ filenames ]
# retrieve or cache include paths:
if self._cfg_share:
# cache/share include list:
hashv = 'inc-path:'+('\x01'.join(filenames))
fileNamesFull = self._cfg_share.get(hashv)
if fileNamesFull is None:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
self._cfg_share[hashv] = fileNamesFull
return fileNamesFull
# don't have sharing:
fileNamesFull = []
for filename in filenames:
fileNamesFull += self.__getIncludesUncached(filename, seen)
return fileNamesFull
def __getIncludesUncached(self, resource, seen=[]):
""" """
Given 1 config resource returns list of included files Given 1 config resource returns list of included files
(recursively) with the original one as well (recursively) with the original one as well
Simple loops are taken care about Simple loops are taken care about
""" """
# Use a short class name ;)
SCPWI = SafeConfigParserWithIncludes SCPWI = SafeConfigParserWithIncludes
parser = SafeConfigParser()
try: try:
if sys.version_info >= (3,2): # pragma: no cover parser, i = self._getSharedSCPWI(resource)
parser.read(resource, encoding='utf-8') if not i:
else: return []
parser.read(resource)
except UnicodeDecodeError, e: except UnicodeDecodeError, e:
logSys.error("Error decoding config file '%s': %s" % (resource, e)) logSys.error("Error decoding config file '%s': %s" % (resource, e))
return [] return []
@ -141,22 +185,60 @@ after = 1.conf
if r in seen: if r in seen:
continue continue
s = seen + [resource] s = seen + [resource]
option_list += SCPWI.getIncludes(r, s) option_list += self._getIncludes(r, s)
# combine lists # combine lists
return newFiles[0][1] + [resource] + newFiles[1][1] return newFiles[0][1] + [resource] + newFiles[1][1]
#print "Includes list for " + resource + " is " + `resources`
getIncludes = staticmethod(getIncludes)
def get_defaults(self):
return self._defaults
def read(self, filenames): def get_sections(self):
fileNamesFull = [] return self._sections
def read(self, filenames, get_includes=True):
if not isinstance(filenames, list): if not isinstance(filenames, list):
filenames = [ filenames ] filenames = [ filenames ]
for filename in filenames: # retrieve (and cache) includes:
fileNamesFull += SafeConfigParserWithIncludes.getIncludes(filename) fileNamesFull = []
logSys.debug("Reading files: %s" % fileNamesFull) if get_includes:
fileNamesFull += self._getIncludes(filenames)
else:
fileNamesFull = filenames
if not fileNamesFull:
return []
logSys.info(" Loading files: %s", fileNamesFull)
if get_includes or len(fileNamesFull) > 1:
# read multiple configs:
ret = []
alld = self.get_defaults()
alls = self.get_sections()
for filename in fileNamesFull:
# read single one, add to return list, use sharing if possible:
cfg, i = self._getSharedSCPWI(filename)
if i:
ret += i
# merge defaults and all sections to self:
alld.update(cfg.get_defaults())
for n, s in cfg.get_sections().iteritems():
if isinstance(s, dict):
s2 = alls.get(n)
if isinstance(s2, dict):
s2.update(s)
else:
alls[n] = s.copy()
else:
alls[n] = s
return ret
# read one config :
if logSys.getEffectiveLevel() <= logLevel:
logSys.log(logLevel, " Reading file: %s", fileNamesFull[0])
# read file(s) :
if sys.version_info >= (3,2): # pragma: no cover if sys.version_info >= (3,2): # pragma: no cover
return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8') return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8')
else: else:
return SafeConfigParser.read(self, fileNamesFull) return SafeConfigParser.read(self, fileNamesFull)

View File

@ -27,24 +27,127 @@ __license__ = "GPL"
import glob, os import glob, os
from ConfigParser import NoOptionError, NoSectionError from ConfigParser import NoOptionError, NoSectionError
from .configparserinc import SafeConfigParserWithIncludes from .configparserinc import SafeConfigParserWithIncludes, logLevel
from ..helpers import getLogger from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
class ConfigReader(SafeConfigParserWithIncludes): class ConfigReader():
"""Generic config reader class.
A caching adapter which automatically reuses already shared configuration.
"""
def __init__(self, use_config=None, share_config=None, **kwargs):
# use given shared config if possible (see read):
self._cfg_share = None
self._cfg = None
if use_config is not None:
self._cfg = use_config
# share config if possible:
if share_config is not None:
self._cfg_share = share_config
self._cfg_share_kwargs = kwargs
self._cfg_share_basedir = None
elif self._cfg is None:
self._cfg = ConfigReaderUnshared(**kwargs)
def setBaseDir(self, basedir):
if self._cfg:
self._cfg.setBaseDir(basedir)
else:
self._cfg_share_basedir = basedir
def getBaseDir(self):
if self._cfg:
return self._cfg.getBaseDir()
else:
return self._cfg_share_basedir
@property
def share_config(self):
return self._cfg_share
def read(self, name, once=True):
""" Overloads a default (not shared) read of config reader.
To prevent mutiple reads of config files with it includes, reads into
the config reader, if it was not yet cached/shared by 'name'.
"""
# already shared ?
if not self._cfg:
self.touch(name)
# performance feature - read once if using shared config reader:
if once and self._cfg.read_cfg_files is not None:
return self._cfg.read_cfg_files
# load:
logSys.info("Loading configs for %s under %s ", name, self._cfg.getBaseDir())
ret = self._cfg.read(name)
# save already read and return:
self._cfg.read_cfg_files = ret
return ret
def touch(self, name=''):
""" Allocates and share a config file by it name.
Automatically allocates unshared or reuses shared handle by given 'name' and
init arguments inside a given shared storage.
"""
if not self._cfg and self._cfg_share is not None:
self._cfg = self._cfg_share.get(name)
if not self._cfg:
self._cfg = ConfigReaderUnshared(share_config=self._cfg_share, **self._cfg_share_kwargs)
if self._cfg_share_basedir is not None:
self._cfg.setBaseDir(self._cfg_share_basedir)
self._cfg_share[name] = self._cfg
else:
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)
def sections(self):
if self._cfg is not None:
return self._cfg.sections()
return []
def has_section(self, sec):
if self._cfg is not None:
return self._cfg.has_section(sec)
return False
def options(self, *args):
if self._cfg is not None:
return self._cfg.options(*args)
return {}
def get(self, sec, opt):
if self._cfg is not None:
return self._cfg.get(sec, opt)
return None
def getOptions(self, *args, **kwargs):
if self._cfg is not None:
return self._cfg.getOptions(*args, **kwargs)
return {}
class ConfigReaderUnshared(SafeConfigParserWithIncludes):
"""Unshared config reader (previously ConfigReader).
Do not use this class (internal not shared/cached represenation).
Use ConfigReader instead.
"""
DEFAULT_BASEDIR = '/etc/fail2ban' DEFAULT_BASEDIR = '/etc/fail2ban'
def __init__(self, basedir=None): def __init__(self, basedir=None, *args, **kwargs):
SafeConfigParserWithIncludes.__init__(self) SafeConfigParserWithIncludes.__init__(self, *args, **kwargs)
self.read_cfg_files = None
self.setBaseDir(basedir) self.setBaseDir(basedir)
self.__opts = None
def setBaseDir(self, basedir): def setBaseDir(self, basedir):
if basedir is None: if basedir is None:
basedir = ConfigReader.DEFAULT_BASEDIR # stock system location basedir = ConfigReaderUnshared.DEFAULT_BASEDIR # stock system location
self._basedir = basedir.rstrip('/') self._basedir = basedir.rstrip('/')
def getBaseDir(self): def getBaseDir(self):
@ -55,7 +158,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
raise ValueError("Base configuration directory %s does not exist " raise ValueError("Base configuration directory %s does not exist "
% self._basedir) % self._basedir)
basename = os.path.join(self._basedir, filename) basename = os.path.join(self._basedir, filename)
logSys.info("Reading configs for %s under %s " % (basename, self._basedir)) logSys.debug("Reading configs for %s under %s " , filename, self._basedir)
config_files = [ basename + ".conf" ] config_files = [ basename + ".conf" ]
# possible further customizations under a .conf.d directory # possible further customizations under a .conf.d directory
@ -71,14 +174,14 @@ class ConfigReader(SafeConfigParserWithIncludes):
if len(config_files): if len(config_files):
# at least one config exists and accessible # at least one config exists and accessible
logSys.debug("Reading config files: " + ', '.join(config_files)) logSys.debug("Reading config files: %s", ', '.join(config_files))
config_files_read = SafeConfigParserWithIncludes.read(self, config_files) config_files_read = SafeConfigParserWithIncludes.read(self, config_files)
missed = [ cf for cf in config_files if cf not in config_files_read ] missed = [ cf for cf in config_files if cf not in config_files_read ]
if missed: if missed:
logSys.error("Could not read config files: " + ', '.join(missed)) logSys.error("Could not read config files: %s", ', '.join(missed))
if config_files_read: if config_files_read:
return True return True
logSys.error("Found no accessible config files for %r under %s" % logSys.error("Found no accessible config files for %r under %s",
( filename, self.getBaseDir() )) ( filename, self.getBaseDir() ))
return False return False
else: else:
@ -98,7 +201,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
# 1 -> the name of the option # 1 -> the name of the option
# 2 -> the default value for the option # 2 -> the default value for the option
def getOptions(self, sec, options, pOptions = None): def getOptions(self, sec, options, pOptions=None):
values = dict() values = dict()
for option in options: for option in options:
try: try:
@ -121,10 +224,8 @@ class ConfigReader(SafeConfigParserWithIncludes):
logSys.warning("'%s' not defined in '%s'. Using default one: %r" logSys.warning("'%s' not defined in '%s'. Using default one: %r"
% (option[1], sec, option[2])) % (option[1], sec, option[2]))
values[option[1]] = option[2] values[option[1]] = option[2]
else: elif logSys.getEffectiveLevel() <= logLevel:
logSys.debug( logSys.log(logLevel, "Non essential option '%s' not defined in '%s'.", option[1], sec)
"Non essential option '%s' not defined in '%s'.",
option[1], sec)
except ValueError: except ValueError:
logSys.warning("Wrong value for '" + option[1] + "' in '" + sec + logSys.warning("Wrong value for '" + option[1] + "' in '" + sec +
"'. Using default one: '" + `option[2]` + "'") "'. Using default one: '" + `option[2]` + "'")
@ -133,12 +234,12 @@ class ConfigReader(SafeConfigParserWithIncludes):
class DefinitionInitConfigReader(ConfigReader): class DefinitionInitConfigReader(ConfigReader):
"""Config reader for files with options grouped in [Definition] and """Config reader for files with options grouped in [Definition] and
[Init] sections. [Init] sections.
Is a base class for readers of filters and actions, where definitions Is a base class for readers of filters and actions, where definitions
in jails might provide custom values for options defined in [Init] in jails might provide custom values for options defined in [Init]
section. section.
""" """
_configOpts = [] _configOpts = []

View File

@ -33,12 +33,20 @@ logSys = getLogger(__name__)
class Configurator: class Configurator:
def __init__(self): def __init__(self, force_enable=False, share_config=None):
self.__settings = dict() self.__settings = dict()
self.__streams = dict() self.__streams = dict()
self.__fail2ban = Fail2banReader() # always share all config readers:
self.__jails = JailsReader() if share_config is None:
share_config = dict()
self.__share_config = share_config
self.__fail2ban = Fail2banReader(share_config=share_config)
self.__jails = JailsReader(force_enable=force_enable, share_config=share_config)
def Reload(self):
# clear all shared handlers:
self.__share_config.clear()
def setBaseDir(self, folderName): def setBaseDir(self, folderName):
self.__fail2ban.setBaseDir(folderName) self.__fail2ban.setBaseDir(folderName)
self.__jails.setBaseDir(folderName) self.__jails.setBaseDir(folderName)

View File

@ -26,7 +26,7 @@ __license__ = "GPL"
import os, shlex import os, shlex
from .configreader import ConfigReader, DefinitionInitConfigReader from .configreader import DefinitionInitConfigReader
from ..server.action import CommandAction from ..server.action import CommandAction
from ..helpers import getLogger from ..helpers import getLogger
@ -40,8 +40,12 @@ class FilterReader(DefinitionInitConfigReader):
["string", "failregex", ""], ["string", "failregex", ""],
] ]
def read(self): def setFile(self, fileName):
return ConfigReader.read(self, os.path.join("filter.d", self._file)) self.__file = fileName
DefinitionInitConfigReader.setFile(self, os.path.join("filter.d", fileName))
def getFile(self):
return self.__file
def convert(self): def convert(self):
stream = list() stream = list()

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
import re, glob, os.path import re, glob, os.path
import json import json
from .configreader import ConfigReader from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader from .filterreader import FilterReader
from .actionreader import ActionReader from .actionreader import ActionReader
from ..helpers import getLogger from ..helpers import getLogger
@ -111,7 +111,7 @@ class JailReader(ConfigReader):
filterName, filterOpt = JailReader.extractOptions( filterName, filterOpt = JailReader.extractOptions(
self.__opts["filter"]) self.__opts["filter"])
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, basedir=self.getBaseDir()) filterName, self.__name, filterOpt, share_config=self.share_config, basedir=self.getBaseDir())
ret = self.__filter.read() ret = self.__filter.read()
if ret: if ret:
self.__filter.getOptions(self.__opts) self.__filter.getOptions(self.__opts)
@ -141,7 +141,7 @@ class JailReader(ConfigReader):
else: else:
action = ActionReader( action = ActionReader(
actName, self.__name, actOpt, actName, self.__name, actOpt,
basedir=self.getBaseDir()) share_config=self.share_config, basedir=self.getBaseDir())
ret = action.read() ret = action.read()
if ret: if ret:
action.getOptions(self.__opts) action.getOptions(self.__opts)
@ -213,7 +213,7 @@ class JailReader(ConfigReader):
if self.__filter: if self.__filter:
stream.extend(self.__filter.convert()) stream.extend(self.__filter.convert())
for action in self.__actions: for action in self.__actions:
if isinstance(action, ConfigReader): if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
stream.extend(action.convert()) stream.extend(action.convert())
else: else:
stream.append(action) stream.append(action)

View File

@ -68,9 +68,10 @@ class JailsReader(ConfigReader):
for sec in sections: for sec in sections:
if sec == 'INCLUDES': if sec == 'INCLUDES':
continue continue
jail = JailReader(sec, basedir=self.getBaseDir(), # use the cfg_share for filter/action caching and the same config for all
force_enable=self.__force_enable) # jails (use_config=...), therefore don't read it here:
jail.read() jail = JailReader(sec, force_enable=self.__force_enable,
share_config=self.share_config, use_config=self._cfg)
ret = jail.getOptions() ret = jail.getOptions()
if ret: if ret:
if jail.isEnabled(): if jail.isEnabled():

View File

@ -21,9 +21,9 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
import os, glob, shutil, tempfile, unittest import os, glob, shutil, tempfile, unittest, re, logging
from ..client.configreader import ConfigReaderUnshared
from ..client.configreader import ConfigReader from ..client import configparserinc
from ..client.jailreader import JailReader from ..client.jailreader import JailReader
from ..client.filterreader import FilterReader from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader from ..client.jailsreader import JailsReader
@ -44,7 +44,7 @@ class ConfigReaderTest(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.d = tempfile.mkdtemp(prefix="f2b-temp") self.d = tempfile.mkdtemp(prefix="f2b-temp")
self.c = ConfigReader(basedir=self.d) self.c = ConfigReaderUnshared(basedir=self.d)
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
@ -335,6 +335,64 @@ class FilterReaderTest(unittest.TestCase):
self.assertRaises(ValueError, FilterReader.convert, filterReader) self.assertRaises(ValueError, FilterReader.convert, filterReader)
class JailsReaderTestCache(LogCaptureTestCase):
def _readWholeConf(self, basedir, force_enable=False, share_config=None):
# read whole configuration like a file2ban-client ...
configurator = Configurator(force_enable=force_enable, share_config=share_config)
configurator.setBaseDir(basedir)
configurator.readEarly()
configurator.getEarlyOptions()
configurator.readAll()
# from here we test a cache with all includes / before / after :
self.assertTrue(configurator.getOptions(None))
def _getLoggedReadCount(self, filematch):
cnt = 0
for s in self.getLog().rsplit('\n'):
if re.match(r"^\s*Reading files?: .*/"+filematch, s):
cnt += 1
return cnt
def testTestJailConfCache(self):
saved_ll = configparserinc.logLevel
configparserinc.logLevel = logging.DEBUG
basedir = tempfile.mkdtemp("fail2ban_conf")
try:
shutil.rmtree(basedir)
shutil.copytree(CONFIG_DIR, basedir)
shutil.copy(CONFIG_DIR + '/jail.conf', basedir + '/jail.local')
shutil.copy(CONFIG_DIR + '/fail2ban.conf', basedir + '/fail2ban.local')
# common sharing handle for this test:
share_cfg = dict()
# read whole configuration like a file2ban-client ...
self._readWholeConf(basedir, share_config=share_cfg)
# how many times jail.local was read:
cnt = self._getLoggedReadCount('jail.local')
# if cnt > 1:
# self.printLog()
self.assertTrue(cnt == 1, "Unexpected count by reading of jail files, cnt = %s" % cnt)
# read whole configuration like a file2ban-client, again ...
# but this time force enable all jails, to check filter and action cached also:
self._readWholeConf(basedir, force_enable=True, share_config=share_cfg)
cnt = self._getLoggedReadCount(r'jail\.local')
# still one (no more reads):
self.assertTrue(cnt == 1, "Unexpected count by second reading of jail files, cnt = %s" % cnt)
# same with filter:
cnt = self._getLoggedReadCount(r'filter\.d/common\.conf')
self.assertTrue(cnt == 1, "Unexpected count by reading of filter files, cnt = %s" % cnt)
# same with action:
cnt = self._getLoggedReadCount(r'action\.d/iptables-common\.conf')
self.assertTrue(cnt == 1, "Unexpected count by reading of action files, cnt = %s" % cnt)
finally:
shutil.rmtree(basedir)
configparserinc.logLevel = saved_ll
class JailsReaderTest(LogCaptureTestCase): class JailsReaderTest(LogCaptureTestCase):
def testProvidingBadBasedir(self): def testProvidingBadBasedir(self):

View File

@ -25,6 +25,7 @@ __license__ = "GPL"
import logging import logging
import os import os
import re import re
import sys
import time import time
import unittest import unittest
from StringIO import StringIO from StringIO import StringIO
@ -111,6 +112,7 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.FilterReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.FilterReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest)) tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest))
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTestCache))
# CSocket and AsyncServer # CSocket and AsyncServer
tests.addTest(unittest.makeSuite(sockettestcase.Socket)) tests.addTest(unittest.makeSuite(sockettestcase.Socket))
# Misc helpers # Misc helpers
@ -204,6 +206,8 @@ class LogCaptureTestCase(unittest.TestCase):
# Let's log everything into a string # Let's log everything into a string
self._log = StringIO() self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)] logSys.handlers = [logging.StreamHandler(self._log)]
if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them!
logSys.handlers += self._old_handlers
logSys.setLevel(getattr(logging, 'DEBUG')) logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self): def tearDown(self):
@ -216,5 +220,8 @@ class LogCaptureTestCase(unittest.TestCase):
def _is_logged(self, s): def _is_logged(self, s):
return s in self._log.getvalue() return s in self._log.getvalue()
def getLog(self):
return self._log.getvalue()
def printLog(self): def printLog(self):
print(self._log.getvalue()) print(self._log.getvalue())