Commit 5514dd78 authored by Michal 'vorner' Vaner's avatar Michal 'vorner' Vaner
Browse files

Merge branch 'work/configuration'

parents cd620bf3 7a3dc628
......@@ -663,6 +663,7 @@ AC_CONFIG_FILES([Makefile
src/lib/python/isc/net/tests/Makefile
src/lib/python/isc/notify/Makefile
src/lib/python/isc/notify/tests/Makefile
src/lib/python/isc/testutils/Makefile
src/lib/config/Makefile
src/lib/config/tests/Makefile
src/lib/config/tests/testdata/Makefile
......
......@@ -194,14 +194,21 @@ class CChannelConnectError(Exception): pass
class BoB:
"""Boss of BIND class."""
def __init__(self, msgq_socket_file=None, nocache=False, verbose=False,
setuid=None, username=None):
def __init__(self, msgq_socket_file=None, data_path=None,
config_filename=None, nocache=False, verbose=False, setuid=None,
username=None, cmdctl_port=None):
"""
Initialize the Boss of BIND. This is a singleton (only one can run).
The msgq_socket_file specifies the UNIX domain socket file that the
msgq process listens on. If verbose is True, then the boss reports
what it is doing.
Data path and config filename are passed trough to config manager
(if provided) and specify the config file to be used.
The cmdctl_port is passed to cmdctl and specify on which port it
should listen.
"""
self.cc_session = None
self.ccs = None
......@@ -219,6 +226,9 @@ class BoB:
self.uid = setuid
self.username = username
self.verbose = verbose
self.data_path = data_path
self.config_filename = config_filename
self.cmdctl_port = cmdctl_port
def config_handler(self, new_config):
# If this is initial update, don't do anything now, leave it to startup
......@@ -390,7 +400,12 @@ class BoB:
Starts the configuration manager process
"""
self.log_starting("b10-cfgmgr")
bind_cfgd = ProcessInfo("b10-cfgmgr", ["b10-cfgmgr"],
args = ["b10-cfgmgr"]
if self.data_path is not None:
args.append("--data-path=" + self.data_path)
if self.config_filename is not None:
args.append("--config-filename=" + self.config_filename)
bind_cfgd = ProcessInfo("b10-cfgmgr", args,
c_channel_env, uid=self.uid,
username=self.username)
self.processes[bind_cfgd.pid] = bind_cfgd
......@@ -500,8 +515,13 @@ class BoB:
self.start_simple("b10-stats", c_channel_env)
def start_cmdctl(self, c_channel_env):
# XXX: we hardcode port 8080
self.start_simple("b10-cmdctl", c_channel_env, 8080)
"""
Starts the command control process
"""
args = ["b10-cmdctl"]
if self.cmdctl_port is not None:
args.append("--port=" + str(self.cmdctl_port))
self.start_process("b10-cmdctl", args, c_channel_env, self.cmdctl_port)
def start_all_processes(self):
"""
......@@ -785,6 +805,50 @@ def process_rename(option, opt_str, value, parser):
"""Function that renames the process if it is requested by a option."""
isc.util.process.rename(value)
def parse_args(args=sys.argv[1:], Parser=OptionParser):
"""
Function for parsing command line arguments. Returns the
options object from OptionParser.
"""
parser = Parser(version=VERSION)
parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
type="string", default=None,
help="UNIX domain socket file the b10-msgq daemon will use")
parser.add_option("-n", "--no-cache", action="store_true", dest="nocache",
default=False, help="disable hot-spot cache in authoritative DNS server")
parser.add_option("-u", "--user", dest="user", type="string", default=None,
help="Change user after startup (must run as root)")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
parser.add_option("--pretty-name", type="string", action="callback",
callback=process_rename,
help="Set the process name (displayed in ps, top, ...)")
parser.add_option("-c", "--config-file", action="store",
dest="config_file", default=None,
help="Configuration database filename")
parser.add_option("-p", "--data-path", dest="data_path",
help="Directory to search for configuration files",
default=None)
parser.add_option("--cmdctl-port", dest="cmdctl_port", type="int",
default=None, help="Port of command control")
parser.add_option("--pid-file", dest="pid_file", type="string",
default=None,
help="file to dump the PID of the BIND 10 process")
(options, args) = parser.parse_args(args)
if options.cmdctl_port is not None:
try:
isc.net.parse.port_parse(options.cmdctl_port)
except ValueError as e:
parser.error(e)
if args:
parser.print_help()
sys.exit(1)
return options
def dump_pid(pid_file):
"""
Dump the PID of the current process to the specified file. If the given
......@@ -814,33 +878,14 @@ def unlink_pid_file(pid_file):
if error.errno is not errno.ENOENT:
raise
def main():
global options
global boss_of_bind
# Enforce line buffering on stdout, even when not a TTY
sys.stdout = io.TextIOWrapper(sys.stdout.detach(), line_buffering=True)
# Parse any command-line options.
parser = OptionParser(version=VERSION)
parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
type="string", default=None,
help="UNIX domain socket file the b10-msgq daemon will use")
parser.add_option("-n", "--no-cache", action="store_true", dest="nocache",
default=False, help="disable hot-spot cache in authoritative DNS server")
parser.add_option("-u", "--user", dest="user", type="string", default=None,
help="Change user after startup (must run as root)")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
parser.add_option("--pretty-name", type="string", action="callback",
callback=process_rename,
help="Set the process name (displayed in ps, top, ...)")
parser.add_option("--pid-file", dest="pid_file", type="string",
default=None,
help="file to dump the PID of the BIND 10 process")
(options, args) = parser.parse_args()
if args:
parser.print_help()
sys.exit(1)
options = parse_args()
# Check user ID.
setuid = None
......@@ -890,8 +935,9 @@ def main():
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
# Go bob!
boss_of_bind = BoB(options.msgq_socket_file, options.nocache,
options.verbose, setuid, username)
boss_of_bind = BoB(options.msgq_socket_file, options.data_path,
options.config_file, options.nocache, options.verbose,
setuid, username, options.cmdctl_port)
startup_result = boss_of_bind.startup()
if startup_result:
sys.stderr.write("[bind10] Error on startup: %s\n" % startup_result)
......
......@@ -48,6 +48,8 @@
<arg><option>-n</option></arg>
<arg><option>-u <replaceable>user</replaceable></option></arg>
<arg><option>-v</option></arg>
<arg><option>-c<replaceable>config-filename</replaceable></option></arg>
<arg><option>-p<replaceable>data_path</replaceable></option></arg>
<arg><option>--msgq-socket-file <replaceable>file</replaceable></option></arg>
<arg><option>--no-cache</option></arg>
<arg><option>--user <replaceable>user</replaceable></option></arg>
......@@ -80,6 +82,31 @@
<para>The arguments are as follows:</para>
<variablelist>
<varlistentry>
<term>
<option>-c</option><replaceable>config-filename</replaceable>,
<option>--config-file</option> <replaceable>config-filename</replaceable>
</term>
<listitem>
<para>The configuration filename to use. Can be either absolute or
relative to data path. In case it is absolute, value of data path is
not considered.</para>
<para>Defaults to b10-config.db.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>
<option>-p</option><replaceable>data-path</replaceable>,
<option>--data-path</option> <replaceable>data-path</replaceable>
</term>
<listitem>
<para>The path where BIND 10 programs look for various data files.
Currently only b10-cfgmgr uses it to locate the configuration file,
but the usage might be extended for other programs and other types
of files.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-m</option> <replaceable>file</replaceable>,
......
from bind10 import ProcessInfo, BoB, dump_pid, unlink_pid_file
from bind10 import ProcessInfo, BoB, parse_args, dump_pid, unlink_pid_file
# XXX: environment tests are currently disabled, due to the preprocessor
# setup that we have now complicating the environment
......@@ -9,6 +9,7 @@ import os
import signal
import socket
from isc.net.addr import IPAddr
from isc.testutils.parse_args import TestOptParser, OptsError
class TestProcessInfo(unittest.TestCase):
def setUp(self):
......@@ -412,6 +413,57 @@ class TestStartStopProcessesBob(unittest.TestCase):
bob.config_handler({'start_auth': True, 'start_resolver': True})
class TestParseArgs(unittest.TestCase):
"""
This tests parsing of arguments of the bind10 master process.
"""
#TODO: Write tests for the original parsing, bad options, etc.
def test_no_opts(self):
"""
Test correct default values when no options are passed.
"""
options = parse_args([], TestOptParser)
self.assertEqual(None, options.data_path)
self.assertEqual(None, options.config_file)
self.assertEqual(None, options.cmdctl_port)
def test_data_path(self):
"""
Test it can parse the data path.
"""
self.assertRaises(OptsError, parse_args, ['-p'], TestOptParser)
self.assertRaises(OptsError, parse_args, ['--data-path'],
TestOptParser)
options = parse_args(['-p', '/data/path'], TestOptParser)
self.assertEqual('/data/path', options.data_path)
options = parse_args(['--data-path=/data/path'], TestOptParser)
self.assertEqual('/data/path', options.data_path)
def test_config_filename(self):
"""
Test it can parse the config switch.
"""
self.assertRaises(OptsError, parse_args, ['-c'], TestOptParser)
self.assertRaises(OptsError, parse_args, ['--config-file'],
TestOptParser)
options = parse_args(['-c', 'config-file'], TestOptParser)
self.assertEqual('config-file', options.config_file)
options = parse_args(['--config-file=config-file'], TestOptParser)
self.assertEqual('config-file', options.config_file)
def test_cmdctl_port(self):
"""
Test it can parse the command control port.
"""
self.assertRaises(OptsError, parse_args, ['--cmdctl-port=abc'],
TestOptParser)
self.assertRaises(OptsError, parse_args, ['--cmdctl-port=100000000'],
TestOptParser)
self.assertRaises(OptsError, parse_args, ['--cmdctl-port'],
TestOptParser)
options = parse_args(['--cmdctl-port=1234'], TestOptParser)
self.assertEqual(1234, options.cmdctl_port)
class TestPIDFile(unittest.TestCase):
def setUp(self):
self.pid_file = '@builddir@' + os.sep + 'bind10.pid'
......
......@@ -26,6 +26,7 @@ import getpass
from optparse import OptionParser
from isc.config.config_data import ConfigData, MultiConfigData
from isc.config.module_spec import ModuleSpec
from isc.testutils.parse_args import TestOptParser, OptsError
from bindctl_main import set_bindctl_options
from bindctl import cmdparse
from bindctl import bindcmd
......@@ -452,23 +453,8 @@ class TestBindCmdInterpreter(unittest.TestCase):
class TestCommandLineOptions(unittest.TestCase):
class FakeParserError(Exception):
"""An exception thrown from FakeOptionParser on parser error.
"""
pass
class FakeOptionParser(OptionParser):
"""This fake class emulates the OptionParser class with customized
error handling for the convenient of tests.
"""
def __init__(self):
OptionParser.__init__(self)
def error(self, msg):
raise TestCommandLineOptions.FakeParserError
def setUp(self):
self.parser = self.FakeOptionParser()
self.parser = TestOptParser()
set_bindctl_options(self.parser)
def test_csv_file_dir(self):
......@@ -481,7 +467,7 @@ class TestCommandLineOptions(unittest.TestCase):
self.assertEqual('some_dir', options.csv_file_dir)
# missing option arg; should trigger parser error.
self.assertRaises(self.FakeParserError, self.parser.parse_args,
self.assertRaises(OptsError, self.parser.parse_args,
['--csv-file-dir'])
if __name__== "__main__":
......
......@@ -22,6 +22,7 @@ from isc.cc import SessionError
import isc.util.process
import signal
import os
from optparse import OptionParser
isc.util.process.rename()
......@@ -41,18 +42,34 @@ if "B10_FROM_SOURCE" in os.environ:
else:
PREFIX = "@prefix@"
DATA_PATH = "@localstatedir@/@PACKAGE@".replace("${prefix}", PREFIX)
DEFAULT_CONFIG_FILE = "b10-config.db"
cm = None
def parse_options(args=sys.argv[1:], Parser=OptionParser):
parser = Parser()
parser.add_option("-p", "--data-path", dest="data_path",
help="Directory to search for configuration files " +
"(default=" + DATA_PATH + ")", default=DATA_PATH)
parser.add_option("-c", "--config-filename", dest="config_file",
help="Configuration database filename " +
"(default=" + DEFAULT_CONFIG_FILE + ")",
default=DEFAULT_CONFIG_FILE)
(options, args) = parser.parse_args(args)
if args:
parser.error("No non-option arguments allowed")
return options
def signal_handler(signal, frame):
global cm
if cm:
cm.running = False
def main():
options = parse_options()
global cm
try:
cm = ConfigManager(DATA_PATH)
cm = ConfigManager(options.data_path, options.config_file)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
cm.read_config()
......
......@@ -41,16 +41,13 @@
</copyright>
</docinfo>
<!--
<refsynopsisdiv>
<cmdsynopsis>
<command></command>
<arg><option></option></arg>
<arg choice="opt"></arg>
<arg choice="opt"></arg>
<command>b10-cfgmgr</command>
<arg><option>-c<replaceable>config-filename</replaceable></option></arg>
<arg><option>-p<replaceable>data_path</replaceable></option></arg>
</cmdsynopsis>
</refsynopsisdiv>
-->
<refsect1>
<title>DESCRIPTION</title>
......@@ -93,24 +90,38 @@
</para>
</refsect1>
<!--
<refsect1>
<title>ARGUMENTS</title>
<para>
<orderedlist numeration="loweralpha">
<para>The arguments are as follows:</para>
<variablelist>
<varlistentry>
<term>
<option>-c</option><replaceable>config-filename</replaceable>,
<option>--config-filename</option> <replaceable>config-filename</replaceable>
</term>
<listitem>
<para>
</para>
<para>The configuration database filename to use. Can be either
absolute or relative to data path.</para>
<para>Defaults to b10-config.db</para>
</listitem>
</varlistentry>
<varlistentry>
<term>
<option>-p</option><replaceable>data-path</replaceable>,
<option>--data-path</option> <replaceable>data-path</replaceable>
</term>
<listitem>
<para>
</para>
<para>The path where BIND 10 looks for files. The
configuration file is looked for here, if it is relative. If it is
absolute, the path is ignored.</para>
</listitem>
</orderedlist>
</para>
</varlistentry>
</variablelist>
</refsect1>
-->
<refsect1>
<title>FILES</title>
<!-- TODO: fix path -->
......
......@@ -20,9 +20,10 @@
import unittest
import os
import sys
from isc.testutils.parse_args import OptsError, TestOptParser
class MyConfigManager:
def __init__(self, path):
def __init__(self, path, filename):
self._path = path
self.read_config_called = False
self.notify_boss_called = False
......@@ -88,6 +89,69 @@ class TestConfigManagerStartup(unittest.TestCase):
sys.modules.pop("b10-cfgmgr")
class TestParseArgs(unittest.TestCase):
"""
Test for the parsing of command line arguments. We provide a different
array to parse instead.
"""
def test_defaults(self):
"""
Test the default values when no options are provided.
"""
# Pass it empty array, not our arguments
b = __import__("b10-cfgmgr")
parsed = b.parse_options([], TestOptParser)
self.assertEqual(b.DATA_PATH, parsed.data_path)
self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
def test_wrong_args(self):
"""
Test it fails when we pass invalid option.
"""
b = __import__("b10-cfgmgr")
self.assertRaises(OptsError, b.parse_options, ['--wrong-option'],
TestOptParser)
def test_not_arg(self):
"""
Test it fails when there's an argument that's not option
(eg. without -- at the beginning).
"""
b = __import__("b10-cfgmgr")
self.assertRaises(OptsError, b.parse_options, ['not-option'],
TestOptParser)
def test_datapath(self):
"""
Test overwriting the data path.
"""
b = __import__("b10-cfgmgr")
parsed = b.parse_options(['--data-path=/path'], TestOptParser)
self.assertEqual('/path', parsed.data_path)
self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
parsed = b.parse_options(['-p', '/path'], TestOptParser)
self.assertEqual('/path', parsed.data_path)
self.assertEqual(b.DEFAULT_CONFIG_FILE, parsed.config_file)
self.assertRaises(OptsError, b.parse_options, ['-p'], TestOptParser)
self.assertRaises(OptsError, b.parse_options, ['--data-path'],
TestOptParser)
def test_db_filename(self):
"""
Test setting the configuration database file.
"""
b = __import__("b10-cfgmgr")
parsed = b.parse_options(['--config-filename=filename'],
TestOptParser)
self.assertEqual(b.DATA_PATH, parsed.data_path)
self.assertEqual("filename", parsed.config_file)
parsed = b.parse_options(['-c', 'filename'], TestOptParser)
self.assertEqual(b.DATA_PATH, parsed.data_path)
self.assertEqual("filename", parsed.config_file)
self.assertRaises(OptsError, b.parse_options, ['-c'], TestOptParser)
self.assertRaises(OptsError, b.parse_options, ['--config-filename'],
TestOptParser)
if __name__ == '__main__':
unittest.main()
......
SUBDIRS = datasrc cc config log net notify util
SUBDIRS = datasrc cc config log net notify util testutils
python_PYTHON = __init__.py
......
......@@ -44,25 +44,36 @@ class ConfigManagerData:
"""This class hold the actual configuration information, and
reads it from and writes it to persistent storage"""
def __init__(self, data_path, file_name = "b10-config.db"):
def __init__(self, data_path, file_name):
"""Initialize the data for the configuration manager, and
set the version and path for the data store. Initializing
this does not yet read the database, a call to
read_from_file is needed for that."""
read_from_file is needed for that.
In case the file_name is absolute, data_path is ignored
and the directory where the file_name lives is used instead.
"""
self.data = {}
self.data['version'] = config_data.BIND10_CONFIG_DATA_VERSION
self.data_path = data_path
self.db_filename = data_path + os.sep + file_name
if os.path.isabs(file_name):
self.db_filename = file_name
self.data_path = os.path.dirname(file_name)
else:
self.db_filename = data_path + os.sep + file_name
self.data_path = data_path
def read_from_file(data_path, file_name):
"""Read the current configuration found in the file file_name.
If file_name is absolute, data_path is ignored. Otherwise
we look for the file_name in data_path directory.
def read_from_file(data_path, file_name = "b10-config.db"):
"""Read the current configuration found in the file at
data_path. If the file does not exist, a
ConfigManagerDataEmpty exception is raised. If there is a
parse error, or if the data in the file has the wrong
version, a ConfigManagerDataReadError is raised. In the first
case, it is probably safe to log and ignore. In the case of
the second exception, the best way is probably to report the
error and stop loading the system."""
If the file does not exist, a ConfigManagerDataEmpty exception is
raised. If there is a parse error, or if the data in the file has
the wrong version, a ConfigManagerDataReadError is raised. In the
first case, it is probably safe to log and ignore. In the case of
the second exception, the best way is probably to report the error
and stop loading the system.
"""
config = ConfigManagerData(data_path, file_name)
file = None
try:
......@@ -142,20 +153,24 @@ class ConfigManagerData:
class ConfigManager:
"""Creates a configuration manager. The data_path is the path
to the directory containing the b10-config.db file.
to the directory containing the configuraton file,
database_filename points to the configuration file.
If session is set, this will be used as the communication
channel session. If not, a new session will be created.
The ability to specify a custom session is for testing purposes
and should not be needed for normal usage."""
def __init__(self, data_path, session = None):
def __init__(self, data_path, database_filename, session=None):
"""Initialize the configuration manager. The data_path string
is the path to the directory where the configuration is
stored (in <data_path>/b10-config.db). Session is an optional
stored (in <data_path>/<database_filename> or in
<database_filename>, if it is absolute). The dabase_filename
is the config file to load. Session is an optional
cc-channel session. If this is not given, a new one is
created"""
created."""
self.data_path = data_path
self.database_filename = database_filename
self.module_specs = {}
self.config = ConfigManagerData(data_path)
self.config = ConfigManagerData(data_path, database_filename)
if session:
self.cc = session
else:
......@@ -223,17 +238,18 @@ class ConfigManager:
return commands
def read_config(self):
"""Read the current configuration from the b10-config.db file
at the path specificied at init()"""
"""Read the current configuration from the file specificied at init()"""
try:
self.config = ConfigManagerData.read_from_file(self.data_path)
self.config = ConfigManagerData.read_from_file(self.data_path,
self.\
database_filename)
except ConfigManagerDataEmpty:
# ok, just start with an empty config
self.config = ConfigManagerData(self.data_path)
self.config = ConfigManagerData(self.data_path,
self.database_filename)
def write_config(self):
"""Write the current configuration to the b10-config.db file
at the path specificied at init()"""
"""Write the current configuration to the file specificied at init()"""
self.config.write_to_file()
def _handle_get_module_spec(self, cmd):
......
......@@ -27,9 +27,20 @@ class TestConfigManagerData(unittest.TestCase):
def setUp(self):
self.data_path = os.environ['CONFIG_TESTDATA_PATH']
self.writable_data_path = os.environ['CONFIG_WR_TESTDATA_PATH']