Config reader's: introduced new syntax `%(section/option)s`, in opposite to extended interpolation of python 3 `${section:option}` work with all supported python version in fail2ban and this syntax is like our another features like `%(known/option)s`, etc.;

Variable `default_backend` switched to `%(default/backend)s`, so totally backwards compatible now, but now the setting of parameter `backend` in default section of `jail.local` can overwrite default backend also.
Test cases extended: test targeted section options "section/option" (default and cross sections options);
pull/1750/head
sebres 2017-04-13 15:20:35 +02:00
parent 5ce8d4f741
commit e52f483557
4 changed files with 97 additions and 42 deletions

View File

@ -7,7 +7,7 @@ after = paths-overrides.local
[DEFAULT]
default_backend = %(known/backend)s
default_backend = %(default/backend)s
sshd_log = %(syslog_authpriv)s
sshd_backend = %(default_backend)s

View File

@ -56,14 +56,8 @@ if sys.version_info >= (3,2):
if section and not (__name__ in map):
map = map.copy() # just to be safe
map['__name__'] = section
try:
return super(BasicInterpolationWithName, self)._interpolate_some(
parser, option, accum, rest, section, map, *args, **kwargs)
except InterpolationMissingOptionError as e:
# fallback: try to wrap missing default options as "known/options":
if not parser._map_known_defaults(section, option, rest, map): # pragma: no cover
raise e
# try again:
# try to wrap section options like %(section/option)s:
parser._map_section_options(section, option, rest, map)
return super(BasicInterpolationWithName, self)._interpolate_some(
parser, option, accum, rest, section, map, *args, **kwargs)
@ -74,16 +68,16 @@ else: # pragma: no cover
# Interpolate missing known/option as option from default section
SafeConfigParser._cp_interpolate_some = SafeConfigParser._interpolate_some
def _interpolate_some(self, option, accum, rest, section, map, *args, **kwargs):
try:
return self._cp_interpolate_some(option, accum, rest, section, map, *args, **kwargs)
except InterpolationMissingOptionError as e:
# fallback: try to wrap missing default options as "known/options":
if self._map_known_defaults(section, option, rest, map): # pragma: no cover
raise e
# try again:
# try to wrap section options like %(section/option)s:
self._map_section_options(section, option, rest, map)
return self._cp_interpolate_some(option, accum, rest, section, map, *args, **kwargs)
SafeConfigParser._interpolate_some = _interpolate_some
try:
from configparser import _UNSET
except ImportError:
_UNSET = object()
# Gets the instance of the logger.
logSys = getLogger(__name__)
logLevel = 7
@ -123,7 +117,8 @@ after = 1.conf
SECTION_NAME = "INCLUDES"
_KNOWN_OPTSUBST_CRE = re.compile(r'%\(known/([^\)]+)\)s')
SECTION_OPT_CRE = re.compile(r'^(\w+)/(.+)$')
SECTION_OPTSUBST_CRE = re.compile(r'%\((\w+/([^\)]+))\)s')
CONDITIONAL_RE = re.compile(r"^(\w+)(\?.+)$")
@ -142,22 +137,43 @@ after = 1.conf
SafeConfigParser.__init__(self, *args, **kwargs)
self._cfg_share = share_config
def _map_known_defaults(self, section, option, rest, map):
""" Fallback: try to wrap missing default options as "known/options"
def _map_section_options(self, section, option, rest, map):
"""
known = SafeConfigParserWithIncludes._KNOWN_OPTSUBST_CRE.findall(rest)
if not known: # pragma: no cover
Interpolates values of the section options (name syntax `%(section/option)s`).
Fallback: try to wrap missing default options as "default/options" resp. "known/options"
"""
if '/' not in rest or '%(' not in rest: # pragma: no cover
return 0
for opt in known:
kopt = 'known/'+opt
if kopt not in map:
soptrep = SafeConfigParserWithIncludes.SECTION_OPTSUBST_CRE.findall(rest)
if not soptrep: # pragma: no cover
return 0
for sopt, opt in soptrep:
if sopt not in map:
sec = sopt[:~len(opt)]
seclwr = sec.lower()
if seclwr != 'default':
if seclwr == 'known':
# try get raw value from known options:
try:
v = self._sections['KNOWN'][opt]
except KeyError:
# fallback to default:
try:
v = self._defaults[opt]
except KeyError:
continue
self._defaults[kopt] = v
try: # for python 2.6 we should duplicate it in map-vars also:
map[kopt] = v
else:
# get substituted value of opt in section:
v = self.get(sec, opt)
else:
try:
v = self._defaults[opt]
except KeyError:
continue
self._defaults[sopt] = v
try: # for some python versions need to duplicate it in map-vars also:
map[sopt] = v
except: pass
return 1
@ -244,6 +260,18 @@ after = 1.conf
def get_sections(self):
return self._sections
def get(self, sec, opt, raw=False, vars={}, fallback=_UNSET):
try:
return SafeConfigParser.get(self, sec, opt, raw=raw, vars=vars)
except:
sopt = SafeConfigParserWithIncludes.SECTION_OPT_CRE.match(opt)
if not sopt: raise
sec, opt = sopt.groups()
if sec.lower() == 'default':
# get default raw value:
return self._defaults[opt]
return SafeConfigParser.get(self, sec, opt, raw=raw, vars=vars)
def options(self, section, withDefault=True):
"""Return a list of option names for the given section name.
@ -303,11 +331,7 @@ after = 1.conf
s2 = alls.get(n)
if isinstance(s2, dict):
# save previous known values, for possible using in local interpolations later:
sk = {}
for k, v in s2.iteritems():
if not k.startswith('known/') and k != '__name__':
sk['known/'+k] = v
s2.update(sk)
self.merge_section('KNOWN', s2, '')
# merge section
s2.update(s)
else:
@ -324,14 +348,18 @@ after = 1.conf
else:
return SafeConfigParser.read(self, fileNamesFull)
def merge_section(self, section, options, pref='known/'):
def merge_section(self, section, options, pref=None):
alls = self.get_sections()
if pref == '':
alls[section].update(options)
try:
sec = alls[section]
except KeyError:
alls[section] = sec = dict()
if not pref:
sec.update(options)
return
sk = {}
for k, v in options.iteritems():
if not k.startswith(pref) and k != '__name__':
sk[pref+k] = v
alls[section].update(sk)
sec.update(sk)

View File

@ -110,7 +110,7 @@ class ConfigReader():
def sections(self):
try:
return self._cfg.sections()
return (n for n in self._cfg.sections() if n != 'KNOWN')
except AttributeError:
return []

View File

@ -162,6 +162,33 @@ c = d ;in line comment
self.assertEqual(self.c.get('DEFAULT', 'b'), 'a')
self.assertEqual(self.c.get('DEFAULT', 'c'), 'd')
def testTargetedSectionOptions(self):
self.assertFalse(self.c.read('g')) # nothing is there yet
self._write("g.conf", value=None, content="""
[DEFAULT]
a = def-a
b = def-b,a:`%(a)s`
c = def-c,b:"%(b)s"
d = def-d-b:"%(known/b)s"
[jail]
a = jail-a-%(test/a)s
b = jail-b-%(test/b)s
[test]
a = test-a-%(default/a)s
b = test-b-%(known/b)s
""")
self.assertTrue(self.c.read('g'))
self.assertEqual(self.c.get('test', 'a'), 'test-a-def-a')
self.assertEqual(self.c.get('test', 'b'), 'test-b-def-b,a:`test-a-def-a`')
self.assertEqual(self.c.get('jail', 'a'), 'jail-a-test-a-def-a')
self.assertEqual(self.c.get('jail', 'b'), 'jail-b-test-b-def-b,a:`test-a-def-a`')
self.assertEqual(self.c.get('jail', 'c'), 'def-c,b:"jail-b-test-b-def-b,a:`test-a-def-a`"')
self.assertEqual(self.c.get('jail', 'd'), 'def-d-b:"def-b,a:`jail-a-test-a-def-a`"')
self.assertEqual(self.c.get('test', 'c'), 'def-c,b:"test-b-def-b,a:`test-a-def-a`"')
self.assertEqual(self.c.get('test', 'd'), 'def-d-b:"def-b,a:`test-a-def-a`"')
self.assertEqual(self.c.get('DEFAULT', 'c'), 'def-c,b:"def-b,a:`def-a`"')
self.assertEqual(self.c.get('DEFAULT', 'd'), 'def-d-b:"def-b,a:`def-a`"')
class JailReaderTest(LogCaptureTestCase):