Allow for dynamic configuration validation rules

This commit is contained in:
Jean-Paul Calderone 2019-06-17 16:44:17 -04:00
parent 4216bd6ed1
commit fb4c5cf91f
4 changed files with 172 additions and 29 deletions

View File

@ -18,7 +18,10 @@ from allmydata.immutable.upload import Uploader
from allmydata.immutable.offloaded import Helper
from allmydata.control import ControlServer
from allmydata.introducer.client import IntroducerClient
from allmydata.util import (hashutil, base32, pollmixin, log, idlib, yamlutil)
from allmydata.util import (
hashutil, base32, pollmixin, log, idlib,
yamlutil, configutil,
)
from allmydata.util.encodingutil import (get_filesystem_encoding,
from_utf8_or_none)
from allmydata.util.abbreviate import parse_abbreviated_size
@ -39,9 +42,9 @@ GiB=1024*MiB
TiB=1024*GiB
PiB=1024*TiB
def _valid_config_sections():
cfg = node._common_config_sections()
cfg.update({
def _valid_config():
cfg = node._common_valid_config()
return cfg.update(configutil.ValidConfiguration({
"client": (
"helper.furl",
"introducer.furl",
@ -93,8 +96,7 @@ def _valid_config_sections():
"local.directory",
"poll_interval",
),
})
return cfg
}))
# this is put into README in new node-directories
CLIENT_README = """
@ -180,7 +182,7 @@ def read_config(basedir, portnumfile, generated_files=[]):
return node.read_config(
basedir, portnumfile,
generated_files=generated_files,
_valid_config_sections=_valid_config_sections,
_valid_config=_valid_config(),
)

View File

@ -25,8 +25,8 @@ from allmydata.util.fileutil import abspath_expanduser_unicode
from allmydata.util.encodingutil import get_filesystem_encoding, quote_output
from allmydata.util import configutil
def _common_config_sections():
return {
def _common_valid_config():
return configutil.ValidConfiguration({
"connections": (
"tcp",
),
@ -63,7 +63,7 @@ def _common_config_sections():
"onion.external_port",
"onion.private_key_file",
),
}
})
# Add our application versions to the data that Foolscap's LogPublisher
# reports.
@ -152,7 +152,7 @@ def create_node_dir(basedir, readme_text):
f.write(readme_text)
def read_config(basedir, portnumfile, generated_files=[], _valid_config_sections=None):
def read_config(basedir, portnumfile, generated_files=[], _valid_config=None):
"""
Read and validate configuration.
@ -163,15 +163,14 @@ def read_config(basedir, portnumfile, generated_files=[], _valid_config_sections
:param list generated_files: a list of automatically-generated
configuration files.
:param dict _valid_config_sections: (internal use, optional) a
dict-of-dicts structure defining valid configuration sections and
keys
:param ValidConfiguration _valid_config: (internal use, optional) a
structure defining valid configuration sections and keys
:returns: :class:`allmydata.node._Config` instance
"""
basedir = abspath_expanduser_unicode(unicode(basedir))
if _valid_config_sections is None:
_valid_config_sections = _common_config_sections
if _valid_config is None:
_valid_config = _common_valid_config()
# complain if there's bad stuff in the config dir
_error_about_old_config_files(basedir, generated_files)
@ -188,7 +187,7 @@ def read_config(basedir, portnumfile, generated_files=[], _valid_config_sections
if e.errno != errno.ENOENT:
raise
configutil.validate_config(config_fname, parser, _valid_config_sections())
configutil.validate_config(config_fname, parser, _valid_config)
# make sure we have a private configuration area
fileutil.make_dirs(os.path.join(basedir, "private"), 0o700)

View File

@ -9,6 +9,16 @@ from .. import client
class ConfigUtilTests(GridTestMixin, unittest.TestCase):
def setUp(self):
super(ConfigUtilTests, self).setUp()
self.static_valid_config = configutil.ValidConfiguration(
dict(node=['valid']),
)
self.dynamic_valid_config = configutil.ValidConfiguration(
dict(),
lambda section_name: section_name == "node",
lambda section_name, item_name: (section_name, item_name) == ("node", "valid"),
)
def test_config_utils(self):
self.basedir = "cli/ConfigUtilTests/test-config-utils"
@ -44,7 +54,32 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
config = configutil.get_config(fname)
# should succeed, no exceptions
configutil.validate_config(fname, config, dict(node=['valid']))
configutil.validate_config(
fname,
config,
self.static_valid_config,
)
def test_config_dynamic_validation_success(self):
"""
A configuration with sections and items that are not matched by the static
validation but are matched by the dynamic validation is considered
valid.
"""
d = self.mktemp()
os.mkdir(d)
fname = os.path.join(d, 'tahoe.cfg')
with open(fname, 'w') as f:
f.write('[node]\nvalid = foo\n')
config = configutil.get_config(fname)
# should succeed, no exceptions
configutil.validate_config(
fname,
config,
self.dynamic_valid_config,
)
def test_config_validation_invalid_item(self):
d = self.mktemp()
@ -58,11 +93,16 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
e = self.assertRaises(
configutil.UnknownConfigError,
configutil.validate_config,
fname, config, dict(node=['valid']),
fname, config,
self.static_valid_config,
)
self.assertIn("section [node] contains unknown option 'invalid'", str(e))
def test_config_validation_invalid_section(self):
"""
A configuration with a section that is matched by neither the static nor
dynamic validators is rejected.
"""
d = self.mktemp()
os.mkdir(d)
fname = os.path.join(d, 'tahoe.cfg')
@ -74,10 +114,53 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
e = self.assertRaises(
configutil.UnknownConfigError,
configutil.validate_config,
fname, config, dict(node=['valid']),
fname, config,
self.static_valid_config,
)
self.assertIn("contains unknown section [invalid]", str(e))
def test_config_dynamic_validation_invalid_section(self):
"""
A configuration with a section that is matched by neither the static nor
dynamic validators is rejected.
"""
d = self.mktemp()
os.mkdir(d)
fname = os.path.join(d, 'tahoe.cfg')
with open(fname, 'w') as f:
f.write('[node]\nvalid = foo\n[invalid]\n')
config = configutil.get_config(fname)
e = self.assertRaises(
configutil.UnknownConfigError,
configutil.validate_config,
fname, config,
self.dynamic_valid_config,
)
self.assertIn("contains unknown section [invalid]", str(e))
def test_config_dynamic_validation_invalid_item(self):
"""
A configuration with a section, item pair that is matched by neither the
static nor dynamic validators is rejected.
"""
d = self.mktemp()
os.mkdir(d)
fname = os.path.join(d, 'tahoe.cfg')
with open(fname, 'w') as f:
f.write('[node]\nvalid = foo\ninvalid = foo\n')
config = configutil.get_config(fname)
e = self.assertRaises(
configutil.UnknownConfigError,
configutil.validate_config,
fname, config,
self.dynamic_valid_config,
)
self.assertIn("section [node] contains unknown option 'invalid'", str(e))
def test_create_client_config(self):
d = self.mktemp()
os.mkdir(d)
@ -97,5 +180,8 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
config = configutil.get_config(fname)
# should succeed, no exceptions
configutil.validate_config(fname, config,
client._valid_config_sections())
configutil.validate_config(
fname,
config,
client._valid_config(),
)

View File

@ -1,6 +1,7 @@
from ConfigParser import SafeConfigParser
import attr
class UnknownConfigError(Exception):
"""
@ -36,15 +37,16 @@ def write_config(tahoe_cfg, config):
finally:
f.close()
def validate_config(fname, cfg, valid_sections):
def validate_config(fname, cfg, valid_config):
"""
raises UnknownConfigError if there are any unknown sections or config
values.
:param ValidConfiguration valid_config: The definition of a valid
configuration.
:raises UnknownConfigError: if there are any unknown sections or config
values.
"""
for section in cfg.sections():
try:
valid_in_section = valid_sections[section]
except KeyError:
if not valid_config.is_valid_section(section):
raise UnknownConfigError(
"'{fname}' contains unknown section [{section}]".format(
fname=fname,
@ -52,7 +54,7 @@ def validate_config(fname, cfg, valid_sections):
)
)
for option in cfg.options(section):
if option not in valid_in_section:
if not valid_config.is_valid_item(section, option):
raise UnknownConfigError(
"'{fname}' section [{section}] contains unknown option '{option}'".format(
fname=fname,
@ -60,3 +62,57 @@ def validate_config(fname, cfg, valid_sections):
option=option,
)
)
@attr.s
class ValidConfiguration(object):
"""
:ivar dict[bytes, tuple[bytes]] _static_valid_sections: A mapping from
valid section names to valid items in those sections.
:ivar _is_valid_section: A callable which accepts a section name as bytes
and returns True if that section name is valid, False otherwise.
:ivar _is_valid_item: A callable which accepts a section name as bytes and
an item name as bytes and returns True if that section, item pair is
valid, False otherwise.
"""
_static_valid_sections = attr.ib()
_is_valid_section = attr.ib(default=lambda section_name: False)
_is_valid_item = attr.ib(default=lambda section_name, item_name: False)
def is_valid_section(self, section_name):
"""
:return: True if the given section name is valid, False otherwise.
"""
return (
section_name in self._static_valid_sections or
self._is_valid_section(section_name)
)
def is_valid_item(self, section_name, item_name):
"""
:return: True if the given section name, ite name pair is valid, False
otherwise.
"""
return (
item_name in self._static_valid_sections.get(section_name, ()) or
self._is_valid_item(section_name, item_name)
)
def update(self, valid_config):
static_valid_sections = self._static_valid_sections.copy()
static_valid_sections.update(valid_config._static_valid_sections)
return ValidConfiguration(
static_valid_sections,
_either(self._is_valid_section, valid_config._is_valid_section),
_either(self._is_valid_item, valid_config._is_valid_item),
)
def _either(f, g):
"""
:return: A function which returns True if either f or g returns True.
"""
return lambda *a, **kw: f(*a, **kw) or g(*a, **kw)