diff --git a/configure.ac b/configure.ac index a2febd2305..a8378cef8a 100644 --- a/configure.ac +++ b/configure.ac @@ -686,6 +686,7 @@ AC_CONFIG_FILES([Makefile src/bin/bindctl/tests/Makefile src/bin/cfgmgr/Makefile src/bin/cfgmgr/plugins/Makefile + src/bin/cfgmgr/plugins/tests/Makefile src/bin/cfgmgr/tests/Makefile src/bin/host/Makefile src/bin/loadzone/Makefile diff --git a/src/bin/cfgmgr/b10-cfgmgr.py.in b/src/bin/cfgmgr/b10-cfgmgr.py.in index 16c8f76961..d91dfca764 100755 --- a/src/bin/cfgmgr/b10-cfgmgr.py.in +++ b/src/bin/cfgmgr/b10-cfgmgr.py.in @@ -18,6 +18,7 @@ import sys; sys.path.append ('@@PYTHONPATH@@') from isc.config.cfgmgr import ConfigManager, ConfigManagerDataReadError +import bind10_config from isc.cc import SessionError import isc.util.process import signal @@ -28,24 +29,10 @@ import os.path isc.util.process.rename() -# If B10_FROM_SOURCE is set in the environment, we use data files -# from a directory relative to the value of that variable, or, if defined, -# relative to the value of B10_FROM_SOURCE_LOCALSTATEDIR. Otherwise -# we use the ones installed on the system. -# B10_FROM_SOURCE_LOCALSTATEDIR is specifically intended to be used for -# tests where we want to use variuos types of configuration within the test -# environment. (We may want to make it even more generic so that the path is -# passed from the boss process) -if "B10_FROM_SOURCE" in os.environ: - if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ: - DATA_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"] - else: - DATA_PATH = os.environ["B10_FROM_SOURCE"] - PLUGIN_PATHS = [DATA_PATH + '/src/bin/cfgmgr/plugins'] -else: - PREFIX = "@prefix@" - DATA_PATH = "@localstatedir@/@PACKAGE@".replace("${prefix}", PREFIX) - PLUGIN_PATHS = ["@prefix@/share/@PACKAGE@/config_plugins"] +# Import some paths from our configuration +DATA_PATH = bind10_config.DATA_PATH +PLUGIN_PATHS = bind10_config.PLUGIN_PATHS +PREFIX = bind10_config.PREFIX DEFAULT_CONFIG_FILE = "b10-config.db" cm = None diff --git a/src/bin/cfgmgr/plugins/Makefile.am b/src/bin/cfgmgr/plugins/Makefile.am index 952fde6edc..d83c2bbd58 100644 --- a/src/bin/cfgmgr/plugins/Makefile.am +++ b/src/bin/cfgmgr/plugins/Makefile.am @@ -1 +1,5 @@ -EXTRA_DIST = README +SUBDIRS = tests +EXTRA_DIST = README tsig_keys.py tsig_keys.spec + +config_plugindir = @prefix@/share/@PACKAGE@/config_plugins +config_plugin_DATA = tsig_keys.py tsig_keys.spec diff --git a/src/bin/cfgmgr/plugins/tests/Makefile.am b/src/bin/cfgmgr/plugins/tests/Makefile.am new file mode 100644 index 0000000000..48a03939d0 --- /dev/null +++ b/src/bin/cfgmgr/plugins/tests/Makefile.am @@ -0,0 +1,19 @@ +PYCOVERAGE_RUN = @PYCOVERAGE_RUN@ +PYTESTS = tsig_keys_test.py + +EXTRA_DIST = $(PYTESTS) + +# test using command-line arguments, so use check-local target instead of TESTS +check-local: +if ENABLE_PYTHON_COVERAGE + touch $(abs_top_srcdir)/.coverage + rm -f .coverage + ${LN_S} $(abs_top_srcdir)/.coverage .coverage +endif + for pytest in $(PYTESTS) ; do \ + echo Running test: $$pytest ; \ + env B10_TEST_PLUGIN_DIR=$(abs_srcdir)/..:$(abs_builddir)/.. \ + env PYTHONPATH=$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python:$(abs_top_builddir)/src/bin/cfgmgr:$(abs_top_builddir)/src/lib/dns/python/.libs \ + $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \ + done + diff --git a/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py b/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py new file mode 100644 index 0000000000..be2921c05f --- /dev/null +++ b/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py @@ -0,0 +1,103 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# Make sure we can load the module, put it into path +import sys +import os +sys.path.extend(os.environ["B10_TEST_PLUGIN_DIR"].split(':')) + +import tsig_keys +import unittest +import isc.config.module_spec + +class TSigKeysTest(unittest.TestCase): + def test_load(self): + """ + Checks the entry point returns the correct values. + """ + (spec, check) = tsig_keys.load() + # It returns the checking function + self.assertEqual(check, tsig_keys.check) + # The plugin stores it's spec + self.assertEqual(spec, tsig_keys.spec) + + def test_spec(self): + """ + Checks the spec is looking sane (doesn't do really deep check here). + """ + spec = tsig_keys.spec + # In python, we don't generally check the type of something, because + # of the duck typing. + # But this is unittest, so we check it does what we intend and + # supplying that's behaving the same but is different is not our + # intention + self.assertTrue(isinstance(spec, isc.config.module_spec.ModuleSpec)) + # Correct name + self.assertEqual("tsig_keys", spec.get_module_name()) + # There are no commands, nobody would handle them anyway + self.assertEqual([], spec.get_commands_spec()) + # There's some nonempty configuration + self.assertNotEqual({}, spec.get_config_spec()) + + def test_missing_keys(self): + """ + Test that missing keys doesn't kill us. There are just no keys there. + """ + self.assertEqual(None, tsig_keys.check({})) + + def test_data_empty(self): + """Check we accept valid config with empty set of tsig keys.""" + self.assertEqual(None, tsig_keys.check({'keys': []})) + + def test_keys_valid(self): + """ + Check we accept some valid keys (we don't check all the algorithms, + that's the job of isc.dns.TSIGKey). + """ + self.assertEqual(None, tsig_keys.check({'keys': + ['testkey:QklORCAxMCBpcyBjb29sCg==', + 'test.key:QklORCAxMCBpcyBjb29sCg==:hmac-sha1']})) + + def test_keys_same_name(self): + """ + Test we reject when we have multiple keys with the same name. + """ + self.assertEqual("Multiple TSIG keys with name 'test.key.'", + tsig_keys.check({'keys': + ['test.key:QklORCAxMCBpcyBjb29sCg==', + 'test.key:b3RoZXIK']})) + + def test_invalid_key(self): + """ + Test we reject invalid key. + """ + self.assertEqual("TSIG: Invalid TSIG key string: invalid.key", + tsig_keys.check({'keys': ['invalid.key']})) + self.assertEqual( + "TSIG: attempt to decode a value not in base64 char set", + tsig_keys.check({'keys': ['invalid.key:123']})) + + def test_bad_format(self): + """ + Test we fail on bad format. We don't really care much how here, though, + as this should not get in trough config manager anyway. + """ + self.assertNotEqual(None, tsig_keys.check({'bad_name': {}})) + self.assertNotEqual(None, tsig_keys.check({'keys': 'not_list'})) + self.assertNotEqual(None, tsig_keys.check({'keys': 42})) + self.assertNotEqual(None, tsig_keys.check({'keys': {}})) + +if __name__ == '__main__': + unittest.main() diff --git a/src/bin/cfgmgr/plugins/tsig_keys.py b/src/bin/cfgmgr/plugins/tsig_keys.py new file mode 100644 index 0000000000..d57e645930 --- /dev/null +++ b/src/bin/cfgmgr/plugins/tsig_keys.py @@ -0,0 +1,50 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# This is the plugin for tsig_keys configuration section. The TSIG keyring +# lives there (eg. all the shared secrets, with some exceptions where there +# are some TSIG keys elsewhere, but these should be removed soon). We do +# sanity checking of user configuration here, simply by trying to construct +# all the keys here. + +from isc.config.module_spec import module_spec_from_file +from isc.util.file import path_search +from pydnspp import TSIGKey, InvalidParameter +from bind10_config import PLUGIN_PATHS +spec = module_spec_from_file(path_search('tsig_keys.spec', PLUGIN_PATHS)) + +def check(config): + # Check the data layout first + errors=[] + if not spec.validate_config(False, config, errors): + return ' '.join(errors) + # Get the list of keys, if any + keys = config.get('keys', []) + # Run through them, check they can be constructed and there are no + # duplicates + keyNames = set() + for key in keys: + try: + name = str(TSIGKey(key).get_key_name()) + except InvalidParameter as e: + return "TSIG: " + str(e) + if name in keyNames: + return "Multiple TSIG keys with name '" + name + "'" + keyNames.add(name) + # No error found, so let's assume it's OK + return None + +def load(): + return (spec, check) diff --git a/src/bin/cfgmgr/plugins/tsig_keys.spec b/src/bin/cfgmgr/plugins/tsig_keys.spec new file mode 100644 index 0000000000..e558dd2b84 --- /dev/null +++ b/src/bin/cfgmgr/plugins/tsig_keys.spec @@ -0,0 +1,21 @@ +{ + "module_spec": { + "module_name": "tsig_keys", + "module_description": "The TSIG keyring is stored here", + "config_data": [ + { + "item_name": "keys", + "item_type": "list", + "item_optional": false, + "item_default": [], + "list_item_spec": { + "item_name": "key", + "item_type": "string", + "item_optional": false, + "item_default": "" + } + } + ], + "commands": [] + } +} diff --git a/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in b/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in index 37cd0f50c1..ea5fc8b519 100644 --- a/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in +++ b/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in @@ -20,6 +20,7 @@ import unittest import os import sys +import bind10_config from isc.testutils.parse_args import OptsError, TestOptParser class MyConfigManager: @@ -110,6 +111,7 @@ class TestConfigManagerStartup(unittest.TestCase): env_var = os.environ["B10_FROM_SOURCE"] os.environ["B10_FROM_SOURCE"] = tmp_env_var + bind10_config.reload() b = __import__("b10-cfgmgr", globals(), locals()) b.PLUGIN_PATH = [] # It's enough to test plugins in one test b.ConfigManager = MyConfigManager @@ -117,6 +119,7 @@ class TestConfigManagerStartup(unittest.TestCase): if env_var != None: os.environ["B10_FROM_SOURCE"] = env_var + bind10_config.reload() sys.modules.pop("b10-cfgmgr") diff --git a/src/lib/python/bind10_config.py.in b/src/lib/python/bind10_config.py.in index 3f2947d7cb..fe4adb51b2 100644 --- a/src/lib/python/bind10_config.py.in +++ b/src/lib/python/bind10_config.py.in @@ -17,7 +17,37 @@ # variables to python scripts and libraries. import os -BIND10_MSGQ_SOCKET_FILE = os.path.join("@localstatedir@", - "@PACKAGE_NAME@", - "msgq_socket").replace("${prefix}", - "@prefix@") +def reload(): + # In a function, for testing purposes + global BIND10_MSGQ_SOCKET_FILE + global DATA_PATH + global PLUGIN_PATHS + global PREFIX + BIND10_MSGQ_SOCKET_FILE = os.path.join("@localstatedir@", + "@PACKAGE_NAME@", + "msgq_socket").replace("${prefix}", + "@prefix@") + + # If B10_FROM_SOURCE is set in the environment, we use data files + # from a directory relative to the value of that variable, or, if defined, + # relative to the value of B10_FROM_SOURCE_LOCALSTATEDIR. Otherwise + # we use the ones installed on the system. + # B10_FROM_SOURCE_LOCALSTATEDIR is specifically intended to be used for + # tests where we want to use variuos types of configuration within the test + # environment. (We may want to make it even more generic so that the path is + # passed from the boss process) + if "B10_FROM_SOURCE" in os.environ: + if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ: + DATA_PATH = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"] + else: + DATA_PATH = os.environ["B10_FROM_SOURCE"] + PLUGIN_PATHS = [DATA_PATH + '/src/bin/cfgmgr/plugins'] + else: + PREFIX = "@prefix@" + DATA_PATH = "@localstatedir@/@PACKAGE@".replace("${prefix}", PREFIX) + PLUGIN_PATHS = ["@prefix@/share/@PACKAGE@/config_plugins"] + # For testing the plugins so they can find their own spec files + if "B10_TEST_PLUGIN_DIR" in os.environ: + PLUGIN_PATHS = os.environ["B10_TEST_PLUGIN_DIR"].split(':') + +reload() diff --git a/src/lib/python/isc/util/Makefile.am b/src/lib/python/isc/util/Makefile.am index 7ab8048823..f6cbb783cf 100644 --- a/src/lib/python/isc/util/Makefile.am +++ b/src/lib/python/isc/util/Makefile.am @@ -1,5 +1,5 @@ SUBDIRS = . tests -python_PYTHON = __init__.py process.py socketserver_mixin.py +python_PYTHON = __init__.py process.py socketserver_mixin.py file.py pythondir = $(pyexecdir)/isc/util diff --git a/src/lib/python/isc/util/file.py b/src/lib/python/isc/util/file.py new file mode 100644 index 0000000000..faef9a84ab --- /dev/null +++ b/src/lib/python/isc/util/file.py @@ -0,0 +1,29 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Various functions for working with files and directories.""" + +from os.path import exists, join + +def path_search(filename, paths): + """ + Searches list of paths to find filename in one of them. The found one will + be returned or IOError will be returned if it isn't found. + """ + for p in paths: + f = join(p, filename) + if exists(f): + return f + raise IOError("'" + filename + "' not found in " + str(paths)) diff --git a/src/lib/python/isc/util/tests/Makefile.am b/src/lib/python/isc/util/tests/Makefile.am index f32fda0043..0ce96de5df 100644 --- a/src/lib/python/isc/util/tests/Makefile.am +++ b/src/lib/python/isc/util/tests/Makefile.am @@ -1,5 +1,5 @@ PYCOVERAGE_RUN = @PYCOVERAGE_RUN@ -PYTESTS = process_test.py socketserver_mixin_test.py +PYTESTS = process_test.py socketserver_mixin_test.py file_test.py EXTRA_DIST = $(PYTESTS) # test using command-line arguments, so use check-local target instead of TESTS diff --git a/src/lib/python/isc/util/tests/file_test.py b/src/lib/python/isc/util/tests/file_test.py new file mode 100644 index 0000000000..fb765d7248 --- /dev/null +++ b/src/lib/python/isc/util/tests/file_test.py @@ -0,0 +1,32 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import isc.util.file +import unittest + +class FileTest(unittest.TestCase): + def test_search_path_find(self): + """Test it returns the first occurence of the file""" + self.assertEqual('./Makefile', + isc.util.file.path_search('Makefile', + ['/no/such/directory/', '.', + '../tests/'])) + + def test_search_path_notfound(self): + """Test it throws an exception when the file can't be found""" + self.assertRaises(IOError, isc.util.file.path_search, 'no file', ['/no/such/directory']) + +if __name__ == "__main__": + unittest.main()