mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-20 05:28:04 +00:00
Allow for dynamic configuration validation rules
This commit is contained in:
parent
4216bd6ed1
commit
fb4c5cf91f
@ -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(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user