mirror of
https://gitlab.isc.org/isc-projects/kea
synced 2025-08-31 14:05:33 +00:00
Merge branch 'trac1290'
This commit is contained in:
@@ -991,6 +991,7 @@ AC_OUTPUT([doc/version.ent
|
||||
src/lib/util/python/mkpywrapper.py
|
||||
src/lib/util/python/gen_wiredata.py
|
||||
src/lib/server_common/tests/data_path.h
|
||||
tests/lettuce/setup_intree_bind10.sh
|
||||
tests/system/conf.sh
|
||||
tests/system/run.sh
|
||||
tests/system/glue/setup.sh
|
||||
|
@@ -675,6 +675,8 @@ class BoB:
|
||||
args = ["b10-cmdctl"]
|
||||
if self.cmdctl_port is not None:
|
||||
args.append("--port=" + str(self.cmdctl_port))
|
||||
if self.verbose:
|
||||
args.append("-v")
|
||||
self.start_process("b10-cmdctl", args, c_channel_env, self.cmdctl_port)
|
||||
|
||||
def start_all_processes(self):
|
||||
|
@@ -45,6 +45,5 @@ export B10_FROM_BUILD
|
||||
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
||||
export BIND10_MSGQ_SOCKET_FILE
|
||||
|
||||
cd ${BIND10_PATH}
|
||||
exec ${PYTHON_EXEC} -O bind10 "$@"
|
||||
exec ${PYTHON_EXEC} -O ${BIND10_PATH}/bind10 "$@"
|
||||
|
||||
|
@@ -71,21 +71,21 @@ Type \"<module_name> <command_name> help\" for help on the specific command.
|
||||
\nAvailable module names: """
|
||||
|
||||
class ValidatedHTTPSConnection(http.client.HTTPSConnection):
|
||||
'''Overrides HTTPSConnection to support certification
|
||||
'''Overrides HTTPSConnection to support certification
|
||||
validation. '''
|
||||
def __init__(self, host, ca_certs):
|
||||
http.client.HTTPSConnection.__init__(self, host)
|
||||
self.ca_certs = ca_certs
|
||||
|
||||
def connect(self):
|
||||
''' Overrides the connect() so that we do
|
||||
''' Overrides the connect() so that we do
|
||||
certificate validation. '''
|
||||
sock = socket.create_connection((self.host, self.port),
|
||||
self.timeout)
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
|
||||
|
||||
req_cert = ssl.CERT_NONE
|
||||
if self.ca_certs:
|
||||
req_cert = ssl.CERT_REQUIRED
|
||||
@@ -95,7 +95,7 @@ class ValidatedHTTPSConnection(http.client.HTTPSConnection):
|
||||
ca_certs=self.ca_certs)
|
||||
|
||||
class BindCmdInterpreter(Cmd):
|
||||
"""simple bindctl example."""
|
||||
"""simple bindctl example."""
|
||||
|
||||
def __init__(self, server_port='localhost:8080', pem_file=None,
|
||||
csv_file_dir=None):
|
||||
@@ -128,29 +128,33 @@ class BindCmdInterpreter(Cmd):
|
||||
socket.gethostname())).encode())
|
||||
digest = session_id.hexdigest()
|
||||
return digest
|
||||
|
||||
|
||||
def run(self):
|
||||
'''Parse commands from user and send them to cmdctl. '''
|
||||
try:
|
||||
if not self.login_to_cmdctl():
|
||||
return
|
||||
return 1
|
||||
|
||||
self.cmdloop()
|
||||
print('\nExit from bindctl')
|
||||
return 0
|
||||
except FailToLogin as err:
|
||||
# error already printed when this was raised, ignoring
|
||||
pass
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
print('\nExit from bindctl')
|
||||
return 0
|
||||
except socket.error as err:
|
||||
print('Failed to send request, the connection is closed')
|
||||
return 1
|
||||
except http.client.CannotSendRequest:
|
||||
print('Can not send request, the connection is busy')
|
||||
return 1
|
||||
|
||||
def _get_saved_user_info(self, dir, file_name):
|
||||
''' Read all the available username and password pairs saved in
|
||||
''' Read all the available username and password pairs saved in
|
||||
file(path is "dir + file_name"), Return value is one list of elements
|
||||
['name', 'password'], If get information failed, empty list will be
|
||||
['name', 'password'], If get information failed, empty list will be
|
||||
returned.'''
|
||||
if (not dir) or (not os.path.exists(dir)):
|
||||
return []
|
||||
@@ -176,7 +180,7 @@ class BindCmdInterpreter(Cmd):
|
||||
if not os.path.exists(dir):
|
||||
os.mkdir(dir, 0o700)
|
||||
|
||||
csvfilepath = dir + file_name
|
||||
csvfilepath = dir + file_name
|
||||
csvfile = open(csvfilepath, 'w')
|
||||
os.chmod(csvfilepath, 0o600)
|
||||
writer = csv.writer(csvfile)
|
||||
@@ -190,7 +194,7 @@ class BindCmdInterpreter(Cmd):
|
||||
return True
|
||||
|
||||
def login_to_cmdctl(self):
|
||||
'''Login to cmdctl with the username and password inputted
|
||||
'''Login to cmdctl with the username and password inputted
|
||||
from user. After the login is sucessful, the username and
|
||||
password will be saved in 'default_user.csv', when run the next
|
||||
time, username and password saved in 'default_user.csv' will be
|
||||
@@ -256,14 +260,14 @@ class BindCmdInterpreter(Cmd):
|
||||
if self.login_to_cmdctl():
|
||||
# successful, so try send again
|
||||
status, reply_msg = self._send_message(url, body)
|
||||
|
||||
|
||||
if reply_msg:
|
||||
return json.loads(reply_msg.decode())
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def send_POST(self, url, post_param = None):
|
||||
|
||||
def send_POST(self, url, post_param = None):
|
||||
'''Send POST request to cmdctl, session id is send with the name
|
||||
'cookie' in header.
|
||||
Format: /module_name/command_name
|
||||
@@ -322,12 +326,12 @@ class BindCmdInterpreter(Cmd):
|
||||
def _validate_cmd(self, cmd):
|
||||
'''validate the parameters and merge some parameters together,
|
||||
merge algorithm is based on the command line syntax, later, if
|
||||
a better command line syntax come out, this function should be
|
||||
updated first.
|
||||
a better command line syntax come out, this function should be
|
||||
updated first.
|
||||
'''
|
||||
if not cmd.module in self.modules:
|
||||
raise CmdUnknownModuleSyntaxError(cmd.module)
|
||||
|
||||
|
||||
module_info = self.modules[cmd.module]
|
||||
if not module_info.has_command_with_name(cmd.command):
|
||||
raise CmdUnknownCmdSyntaxError(cmd.module, cmd.command)
|
||||
@@ -335,17 +339,17 @@ class BindCmdInterpreter(Cmd):
|
||||
command_info = module_info.get_command_with_name(cmd.command)
|
||||
manda_params = command_info.get_mandatory_param_names()
|
||||
all_params = command_info.get_param_names()
|
||||
|
||||
|
||||
# If help is entered, don't do further parameter validation.
|
||||
for val in cmd.params.keys():
|
||||
if val == "help":
|
||||
return
|
||||
|
||||
params = cmd.params.copy()
|
||||
if not params and manda_params:
|
||||
raise CmdMissParamSyntaxError(cmd.module, cmd.command, manda_params[0])
|
||||
|
||||
params = cmd.params.copy()
|
||||
if not params and manda_params:
|
||||
raise CmdMissParamSyntaxError(cmd.module, cmd.command, manda_params[0])
|
||||
elif params and not all_params:
|
||||
raise CmdUnknownParamSyntaxError(cmd.module, cmd.command,
|
||||
raise CmdUnknownParamSyntaxError(cmd.module, cmd.command,
|
||||
list(params.keys())[0])
|
||||
elif params:
|
||||
param_name = None
|
||||
@@ -376,7 +380,7 @@ class BindCmdInterpreter(Cmd):
|
||||
param_name = command_info.get_param_name_by_position(name, param_count)
|
||||
cmd.params[param_name] = cmd.params[name]
|
||||
del cmd.params[name]
|
||||
|
||||
|
||||
elif not name in all_params:
|
||||
raise CmdUnknownParamSyntaxError(cmd.module, cmd.command, name)
|
||||
|
||||
@@ -385,7 +389,7 @@ class BindCmdInterpreter(Cmd):
|
||||
if not name in params and not param_nr in params:
|
||||
raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
|
||||
param_nr += 1
|
||||
|
||||
|
||||
# Convert parameter value according parameter spec file.
|
||||
# Ignore check for commands belongs to module 'config'
|
||||
if cmd.module != CONFIG_MODULE_NAME:
|
||||
@@ -394,9 +398,9 @@ class BindCmdInterpreter(Cmd):
|
||||
try:
|
||||
cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
|
||||
except isc.cc.data.DataTypeError as e:
|
||||
raise isc.cc.data.DataTypeError('Invalid parameter value for \"%s\", the type should be \"%s\" \n'
|
||||
raise isc.cc.data.DataTypeError('Invalid parameter value for \"%s\", the type should be \"%s\" \n'
|
||||
% (param_name, param_spec['item_type']) + str(e))
|
||||
|
||||
|
||||
def _handle_cmd(self, cmd):
|
||||
'''Handle a command entered by the user'''
|
||||
if cmd.command == "help" or ("help" in cmd.params.keys()):
|
||||
@@ -418,7 +422,7 @@ class BindCmdInterpreter(Cmd):
|
||||
def add_module_info(self, module_info):
|
||||
'''Add the information about one module'''
|
||||
self.modules[module_info.name] = module_info
|
||||
|
||||
|
||||
def get_module_names(self):
|
||||
'''Return the names of all known modules'''
|
||||
return list(self.modules.keys())
|
||||
@@ -450,15 +454,15 @@ class BindCmdInterpreter(Cmd):
|
||||
subsequent_indent=" " +
|
||||
" " * CONST_BINDCTL_HELP_INDENT_WIDTH,
|
||||
width=70))
|
||||
|
||||
|
||||
def onecmd(self, line):
|
||||
if line == 'EOF' or line.lower() == "quit":
|
||||
self.conn.close()
|
||||
return True
|
||||
|
||||
|
||||
if line == 'h':
|
||||
line = 'help'
|
||||
|
||||
|
||||
Cmd.onecmd(self, line)
|
||||
|
||||
def remove_prefix(self, list, prefix):
|
||||
@@ -486,7 +490,7 @@ class BindCmdInterpreter(Cmd):
|
||||
cmd = BindCmdParse(cur_line)
|
||||
if not cmd.params and text:
|
||||
hints = self._get_command_startswith(cmd.module, text)
|
||||
else:
|
||||
else:
|
||||
hints = self._get_param_startswith(cmd.module, cmd.command,
|
||||
text)
|
||||
if cmd.module == CONFIG_MODULE_NAME:
|
||||
@@ -502,8 +506,8 @@ class BindCmdInterpreter(Cmd):
|
||||
|
||||
except CmdMissCommandNameFormatError as e:
|
||||
if not text.strip(): # command name is empty
|
||||
hints = self.modules[e.module].get_command_names()
|
||||
else:
|
||||
hints = self.modules[e.module].get_command_names()
|
||||
else:
|
||||
hints = self._get_module_startswith(text)
|
||||
|
||||
except CmdCommandNameFormatError as e:
|
||||
@@ -523,36 +527,37 @@ class BindCmdInterpreter(Cmd):
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_module_startswith(self, text):
|
||||
|
||||
def _get_module_startswith(self, text):
|
||||
return [module
|
||||
for module in self.modules
|
||||
for module in self.modules
|
||||
if module.startswith(text)]
|
||||
|
||||
|
||||
def _get_command_startswith(self, module, text):
|
||||
if module in self.modules:
|
||||
return [command
|
||||
for command in self.modules[module].get_command_names()
|
||||
if command.startswith(text)]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _get_param_startswith(self, module, command, text):
|
||||
if module in self.modules:
|
||||
module_info = self.modules[module]
|
||||
if command in module_info.get_command_names():
|
||||
return [command
|
||||
for command in self.modules[module].get_command_names()
|
||||
if command.startswith(text)]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _get_param_startswith(self, module, command, text):
|
||||
if module in self.modules:
|
||||
module_info = self.modules[module]
|
||||
if command in module_info.get_command_names():
|
||||
cmd_info = module_info.get_command_with_name(command)
|
||||
params = cmd_info.get_param_names()
|
||||
params = cmd_info.get_param_names()
|
||||
hint = []
|
||||
if text:
|
||||
if text:
|
||||
hint = [val for val in params if val.startswith(text)]
|
||||
else:
|
||||
hint = list(params)
|
||||
|
||||
|
||||
if len(hint) == 1 and hint[0] != "help":
|
||||
hint[0] = hint[0] + " ="
|
||||
|
||||
hint[0] = hint[0] + " ="
|
||||
|
||||
return hint
|
||||
|
||||
return []
|
||||
@@ -569,24 +574,24 @@ class BindCmdInterpreter(Cmd):
|
||||
self._print_correct_usage(err)
|
||||
except isc.cc.data.DataTypeError as err:
|
||||
print("Error! ", err)
|
||||
|
||||
def _print_correct_usage(self, ept):
|
||||
|
||||
def _print_correct_usage(self, ept):
|
||||
if isinstance(ept, CmdUnknownModuleSyntaxError):
|
||||
self.do_help(None)
|
||||
|
||||
|
||||
elif isinstance(ept, CmdUnknownCmdSyntaxError):
|
||||
self.modules[ept.module].module_help()
|
||||
|
||||
|
||||
elif isinstance(ept, CmdMissParamSyntaxError) or \
|
||||
isinstance(ept, CmdUnknownParamSyntaxError):
|
||||
self.modules[ept.module].command_help(ept.command)
|
||||
|
||||
|
||||
|
||||
|
||||
def _append_space_to_hint(self):
|
||||
"""Append one space at the end of complete hint."""
|
||||
self.hint = [(val + " ") for val in self.hint]
|
||||
|
||||
|
||||
|
||||
|
||||
def _handle_help(self, cmd):
|
||||
if cmd.command == "help":
|
||||
self.modules[cmd.module].module_help()
|
||||
|
@@ -146,4 +146,5 @@ if __name__ == '__main__':
|
||||
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
|
||||
csv_file_dir=options.csv_file_dir)
|
||||
prepare_config_commands(tool)
|
||||
tool.run()
|
||||
result = tool.run()
|
||||
sys.exit(result)
|
||||
|
@@ -31,14 +31,14 @@ from bindctl_main import set_bindctl_options
|
||||
from bindctl import cmdparse
|
||||
from bindctl import bindcmd
|
||||
from bindctl.moduleinfo import *
|
||||
from bindctl.exception import *
|
||||
from bindctl.exception import *
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from mycollections import OrderedDict
|
||||
|
||||
class TestCmdLex(unittest.TestCase):
|
||||
|
||||
|
||||
def my_assert_raise(self, exception_type, cmd_line):
|
||||
self.assertRaises(exception_type, cmdparse.BindCmdParse, cmd_line)
|
||||
|
||||
@@ -48,13 +48,13 @@ class TestCmdLex(unittest.TestCase):
|
||||
assert cmd.module == "zone"
|
||||
assert cmd.command == "add"
|
||||
self.assertEqual(len(cmd.params), 0)
|
||||
|
||||
|
||||
|
||||
|
||||
def testCommandWithParameters(self):
|
||||
lines = {"zone add zone_name = cnnic.cn, file = cnnic.cn.file master=1.1.1.1",
|
||||
"zone add zone_name = \"cnnic.cn\", file ='cnnic.cn.file' master=1.1.1.1 ",
|
||||
"zone add zone_name = 'cnnic.cn\", file ='cnnic.cn.file' master=1.1.1.1, " }
|
||||
|
||||
|
||||
for cmd_line in lines:
|
||||
cmd = cmdparse.BindCmdParse(cmd_line)
|
||||
assert cmd.module == "zone"
|
||||
@@ -75,7 +75,7 @@ class TestCmdLex(unittest.TestCase):
|
||||
cmd = cmdparse.BindCmdParse('zone cmd name = 1\"\'34**&2 ,value= 44\"\'\"')
|
||||
self.assertEqual(cmd.params['name'], '1\"\'34**&2')
|
||||
self.assertEqual(cmd.params['value'], '44\"\'\"')
|
||||
|
||||
|
||||
cmd = cmdparse.BindCmdParse('zone cmd name = 1\'34**&2value=44\"\'\" value = \"==============\'')
|
||||
self.assertEqual(cmd.params['name'], '1\'34**&2value=44\"\'\"')
|
||||
self.assertEqual(cmd.params['value'], '==============')
|
||||
@@ -83,34 +83,34 @@ class TestCmdLex(unittest.TestCase):
|
||||
cmd = cmdparse.BindCmdParse('zone cmd name = \"1234, 567890 \" value ==&*/')
|
||||
self.assertEqual(cmd.params['name'], '1234, 567890 ')
|
||||
self.assertEqual(cmd.params['value'], '=&*/')
|
||||
|
||||
|
||||
def testCommandWithListParam(self):
|
||||
cmd = cmdparse.BindCmdParse("zone set zone_name='cnnic.cn', master='1.1.1.1, 2.2.2.2'")
|
||||
assert cmd.params["master"] == '1.1.1.1, 2.2.2.2'
|
||||
|
||||
assert cmd.params["master"] == '1.1.1.1, 2.2.2.2'
|
||||
|
||||
def testCommandWithHelpParam(self):
|
||||
cmd = cmdparse.BindCmdParse("zone add help")
|
||||
assert cmd.params["help"] == "help"
|
||||
|
||||
|
||||
cmd = cmdparse.BindCmdParse("zone add help *&)&)*&&$#$^%")
|
||||
assert cmd.params["help"] == "help"
|
||||
self.assertEqual(len(cmd.params), 1)
|
||||
|
||||
|
||||
|
||||
def testCmdModuleNameFormatError(self):
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "zone=good")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "zo/ne")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "zo/ne")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "=zone")
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "zone,")
|
||||
|
||||
|
||||
self.my_assert_raise(CmdModuleNameFormatError, "zone,")
|
||||
|
||||
|
||||
def testCmdMissCommandNameFormatError(self):
|
||||
self.my_assert_raise(CmdMissCommandNameFormatError, "zone")
|
||||
self.my_assert_raise(CmdMissCommandNameFormatError, "zone ")
|
||||
self.my_assert_raise(CmdMissCommandNameFormatError, "help ")
|
||||
|
||||
|
||||
|
||||
|
||||
def testCmdCommandNameFormatError(self):
|
||||
self.my_assert_raise(CmdCommandNameFormatError, "zone =d")
|
||||
self.my_assert_raise(CmdCommandNameFormatError, "zone z=d")
|
||||
@@ -119,11 +119,11 @@ class TestCmdLex(unittest.TestCase):
|
||||
self.my_assert_raise(CmdCommandNameFormatError, "zone zdd/ \"")
|
||||
|
||||
class TestCmdSyntax(unittest.TestCase):
|
||||
|
||||
|
||||
def _create_bindcmd(self):
|
||||
"""Create one bindcmd"""
|
||||
|
||||
tool = bindcmd.BindCmdInterpreter()
|
||||
|
||||
tool = bindcmd.BindCmdInterpreter()
|
||||
string_spec = { 'item_type' : 'string',
|
||||
'item_optional' : False,
|
||||
'item_default' : ''}
|
||||
@@ -135,40 +135,40 @@ class TestCmdSyntax(unittest.TestCase):
|
||||
load_cmd = CommandInfo(name = "load")
|
||||
load_cmd.add_param(zone_file_param)
|
||||
load_cmd.add_param(zone_name)
|
||||
|
||||
param_master = ParamInfo(name = "master", optional = True, param_spec = string_spec)
|
||||
param_master = ParamInfo(name = "port", optional = True, param_spec = int_spec)
|
||||
param_allow_update = ParamInfo(name = "allow_update", optional = True, param_spec = string_spec)
|
||||
|
||||
param_master = ParamInfo(name = "master", optional = True, param_spec = string_spec)
|
||||
param_master = ParamInfo(name = "port", optional = True, param_spec = int_spec)
|
||||
param_allow_update = ParamInfo(name = "allow_update", optional = True, param_spec = string_spec)
|
||||
set_cmd = CommandInfo(name = "set")
|
||||
set_cmd.add_param(param_master)
|
||||
set_cmd.add_param(param_allow_update)
|
||||
set_cmd.add_param(zone_name)
|
||||
|
||||
reload_all_cmd = CommandInfo(name = "reload_all")
|
||||
|
||||
zone_module = ModuleInfo(name = "zone")
|
||||
|
||||
reload_all_cmd = CommandInfo(name = "reload_all")
|
||||
|
||||
zone_module = ModuleInfo(name = "zone")
|
||||
zone_module.add_command(load_cmd)
|
||||
zone_module.add_command(set_cmd)
|
||||
zone_module.add_command(reload_all_cmd)
|
||||
|
||||
|
||||
tool.add_module_info(zone_module)
|
||||
return tool
|
||||
|
||||
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.bindcmd = self._create_bindcmd()
|
||||
|
||||
|
||||
|
||||
|
||||
def no_assert_raise(self, cmd_line):
|
||||
cmd = cmdparse.BindCmdParse(cmd_line)
|
||||
self.bindcmd._validate_cmd(cmd)
|
||||
|
||||
|
||||
self.bindcmd._validate_cmd(cmd)
|
||||
|
||||
|
||||
def my_assert_raise(self, exception_type, cmd_line):
|
||||
cmd = cmdparse.BindCmdParse(cmd_line)
|
||||
self.assertRaises(exception_type, self.bindcmd._validate_cmd, cmd)
|
||||
|
||||
|
||||
self.assertRaises(exception_type, self.bindcmd._validate_cmd, cmd)
|
||||
|
||||
|
||||
def testValidateSuccess(self):
|
||||
self.no_assert_raise("zone load zone_file='cn' zone_name='cn'")
|
||||
self.no_assert_raise("zone load zone_file='cn', zone_name='cn', ")
|
||||
@@ -178,27 +178,27 @@ class TestCmdSyntax(unittest.TestCase):
|
||||
self.no_assert_raise("zone set allow_update='1.1.1.1' zone_name='cn'")
|
||||
self.no_assert_raise("zone set zone_name='cn'")
|
||||
self.my_assert_raise(isc.cc.data.DataTypeError, "zone set zone_name ='cn', port='cn'")
|
||||
self.no_assert_raise("zone reload_all")
|
||||
|
||||
|
||||
self.no_assert_raise("zone reload_all")
|
||||
|
||||
|
||||
def testCmdUnknownModuleSyntaxError(self):
|
||||
self.my_assert_raise(CmdUnknownModuleSyntaxError, "zoned d")
|
||||
self.my_assert_raise(CmdUnknownModuleSyntaxError, "dd dd ")
|
||||
|
||||
|
||||
|
||||
|
||||
def testCmdUnknownCmdSyntaxError(self):
|
||||
self.my_assert_raise(CmdUnknownCmdSyntaxError, "zone dd")
|
||||
|
||||
|
||||
def testCmdMissParamSyntaxError(self):
|
||||
self.my_assert_raise(CmdMissParamSyntaxError, "zone load zone_file='cn'")
|
||||
self.my_assert_raise(CmdMissParamSyntaxError, "zone load zone_name='cn'")
|
||||
self.my_assert_raise(CmdMissParamSyntaxError, "zone set allow_update='1.1.1.1'")
|
||||
self.my_assert_raise(CmdMissParamSyntaxError, "zone set ")
|
||||
|
||||
|
||||
def testCmdUnknownParamSyntaxError(self):
|
||||
self.my_assert_raise(CmdUnknownParamSyntaxError, "zone load zone_d='cn'")
|
||||
self.my_assert_raise(CmdUnknownParamSyntaxError, "zone reload_all zone_name = 'cn'")
|
||||
|
||||
self.my_assert_raise(CmdUnknownParamSyntaxError, "zone reload_all zone_name = 'cn'")
|
||||
|
||||
class TestModuleInfo(unittest.TestCase):
|
||||
|
||||
def test_get_param_name_by_position(self):
|
||||
@@ -212,36 +212,36 @@ class TestModuleInfo(unittest.TestCase):
|
||||
self.assertEqual('sex', cmd.get_param_name_by_position(2, 3))
|
||||
self.assertEqual('data', cmd.get_param_name_by_position(2, 4))
|
||||
self.assertEqual('data', cmd.get_param_name_by_position(2, 4))
|
||||
|
||||
|
||||
self.assertRaises(KeyError, cmd.get_param_name_by_position, 4, 4)
|
||||
|
||||
|
||||
|
||||
|
||||
class TestNameSequence(unittest.TestCase):
|
||||
"""
|
||||
Test if the module/command/parameters is saved in the order creation
|
||||
"""
|
||||
|
||||
|
||||
def _create_bindcmd(self):
|
||||
"""Create one bindcmd"""
|
||||
|
||||
"""Create one bindcmd"""
|
||||
|
||||
self._cmd = CommandInfo(name = "load")
|
||||
self.module = ModuleInfo(name = "zone")
|
||||
self.tool = bindcmd.BindCmdInterpreter()
|
||||
self.tool = bindcmd.BindCmdInterpreter()
|
||||
for random_str in self.random_names:
|
||||
self._cmd.add_param(ParamInfo(name = random_str))
|
||||
self.module.add_command(CommandInfo(name = random_str))
|
||||
self.tool.add_module_info(ModuleInfo(name = random_str))
|
||||
|
||||
self.tool.add_module_info(ModuleInfo(name = random_str))
|
||||
|
||||
def setUp(self):
|
||||
self.random_names = ['1erdfeDDWsd', '3fe', '2009erd', 'Fe231', 'tere142', 'rei8WD']
|
||||
self._create_bindcmd()
|
||||
|
||||
def testSequence(self):
|
||||
|
||||
def testSequence(self):
|
||||
param_names = self._cmd.get_param_names()
|
||||
cmd_names = self.module.get_command_names()
|
||||
module_names = self.tool.get_module_names()
|
||||
|
||||
|
||||
i = 0
|
||||
while i < len(self.random_names):
|
||||
assert self.random_names[i] == param_names[i+1]
|
||||
@@ -342,7 +342,7 @@ class TestConfigCommands(unittest.TestCase):
|
||||
# validate log message for socket.err
|
||||
socket_err_output = io.StringIO()
|
||||
sys.stdout = socket_err_output
|
||||
self.assertRaises(None, self.tool.run())
|
||||
self.assertEqual(1, self.tool.run())
|
||||
self.assertEqual("Failed to send request, the connection is closed\n",
|
||||
socket_err_output.getvalue())
|
||||
socket_err_output.close()
|
||||
@@ -350,7 +350,7 @@ class TestConfigCommands(unittest.TestCase):
|
||||
# validate log message for http.client.CannotSendRequest
|
||||
cannot_send_output = io.StringIO()
|
||||
sys.stdout = cannot_send_output
|
||||
self.assertRaises(None, self.tool.run())
|
||||
self.assertEqual(1, self.tool.run())
|
||||
self.assertEqual("Can not send request, the connection is busy\n",
|
||||
cannot_send_output.getvalue())
|
||||
cannot_send_output.close()
|
||||
@@ -472,4 +472,4 @@ class TestCommandLineOptions(unittest.TestCase):
|
||||
|
||||
if __name__== "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
@@ -17,12 +17,12 @@
|
||||
|
||||
''' cmdctl module is the configuration entry point for all commands from bindctl
|
||||
or some other web tools client of bind10. cmdctl is pure https server which provi-
|
||||
des RESTful API. When command client connecting with cmdctl, it should first login
|
||||
with legal username and password.
|
||||
When cmdctl starting up, it will collect command specification and
|
||||
des RESTful API. When command client connecting with cmdctl, it should first login
|
||||
with legal username and password.
|
||||
When cmdctl starting up, it will collect command specification and
|
||||
configuration specification/data of other available modules from configmanager, then
|
||||
wait for receiving request from client, parse the request and resend the request to
|
||||
the proper module. When getting the request result from the module, send back the
|
||||
the proper module. When getting the request result from the module, send back the
|
||||
resut to client.
|
||||
'''
|
||||
|
||||
@@ -81,16 +81,16 @@ SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
|
||||
|
||||
class CmdctlException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
'''https connection request handler.
|
||||
Currently only GET and POST are supported. '''
|
||||
def do_GET(self):
|
||||
'''The client should send its session id in header with
|
||||
'''The client should send its session id in header with
|
||||
the name 'cookie'
|
||||
'''
|
||||
self.session_id = self.headers.get('cookie')
|
||||
rcode, reply = http.client.OK, []
|
||||
rcode, reply = http.client.OK, []
|
||||
if self._is_session_valid():
|
||||
if self._is_user_logged_in():
|
||||
rcode, reply = self._handle_get_request()
|
||||
@@ -106,16 +106,16 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
def _handle_get_request(self):
|
||||
'''Currently only support the following three url GET request '''
|
||||
id, module = self._parse_request_path()
|
||||
return self.server.get_reply_data_for_GET(id, module)
|
||||
return self.server.get_reply_data_for_GET(id, module)
|
||||
|
||||
def _is_session_valid(self):
|
||||
return self.session_id
|
||||
return self.session_id
|
||||
|
||||
def _is_user_logged_in(self):
|
||||
login_time = self.server.user_sessions.get(self.session_id)
|
||||
if not login_time:
|
||||
return False
|
||||
|
||||
|
||||
idle_time = time.time() - login_time
|
||||
if idle_time > self.server.idle_timeout:
|
||||
return False
|
||||
@@ -125,7 +125,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def _parse_request_path(self):
|
||||
'''Parse the url, the legal url should like /ldh or /ldh/ldh '''
|
||||
groups = URL_PATTERN.match(self.path)
|
||||
groups = URL_PATTERN.match(self.path)
|
||||
if not groups:
|
||||
return (None, None)
|
||||
else:
|
||||
@@ -133,8 +133,8 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
'''Process POST request. '''
|
||||
'''Process user login and send command to proper module
|
||||
The client should send its session id in header with
|
||||
'''Process user login and send command to proper module
|
||||
The client should send its session id in header with
|
||||
the name 'cookie'
|
||||
'''
|
||||
self.session_id = self.headers.get('cookie')
|
||||
@@ -148,7 +148,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
rcode, reply = http.client.UNAUTHORIZED, ["please login"]
|
||||
else:
|
||||
rcode, reply = http.client.BAD_REQUEST, ["session isn't valid"]
|
||||
|
||||
|
||||
self.send_response(rcode)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(reply).encode())
|
||||
@@ -169,12 +169,12 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
length = self.headers.get('Content-Length')
|
||||
|
||||
if not length:
|
||||
return False, ["invalid username or password"]
|
||||
return False, ["invalid username or password"]
|
||||
|
||||
try:
|
||||
user_info = json.loads((self.rfile.read(int(length))).decode())
|
||||
except:
|
||||
return False, ["invalid username or password"]
|
||||
return False, ["invalid username or password"]
|
||||
|
||||
user_name = user_info.get('username')
|
||||
if not user_name:
|
||||
@@ -193,7 +193,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
return False, ["username or password error"]
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
|
||||
def _handle_post_request(self):
|
||||
'''Handle all the post request from client. '''
|
||||
@@ -215,7 +215,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
if rcode != 0:
|
||||
ret = http.client.BAD_REQUEST
|
||||
return ret, reply
|
||||
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
'''Rewrite the log request function, log nothing.'''
|
||||
pass
|
||||
@@ -239,11 +239,11 @@ class CommandControl():
|
||||
|
||||
def _setup_session(self):
|
||||
'''Setup the session for receving the commands
|
||||
sent from other modules. There are two sessions
|
||||
for cmdctl, one(self.module_cc) is used for receiving
|
||||
commands sent from other modules, another one (self._cc)
|
||||
is used to send the command from Bindctl or other tools
|
||||
to proper modules.'''
|
||||
sent from other modules. There are two sessions
|
||||
for cmdctl, one(self.module_cc) is used for receiving
|
||||
commands sent from other modules, another one (self._cc)
|
||||
is used to send the command from Bindctl or other tools
|
||||
to proper modules.'''
|
||||
self._cc = isc.cc.Session()
|
||||
self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
|
||||
self.config_handler,
|
||||
@@ -251,7 +251,7 @@ class CommandControl():
|
||||
self._module_name = self._module_cc.get_module_spec().get_module_name()
|
||||
self._cmdctl_config_data = self._module_cc.get_full_config()
|
||||
self._module_cc.start()
|
||||
|
||||
|
||||
def _accounts_file_check(self, filepath):
|
||||
''' Check whether the accounts file is valid, each row
|
||||
should be a list with 3 items.'''
|
||||
@@ -288,7 +288,7 @@ class CommandControl():
|
||||
errstr = self._accounts_file_check(new_config[key])
|
||||
else:
|
||||
errstr = 'unknown config item: ' + key
|
||||
|
||||
|
||||
if errstr != None:
|
||||
logger.error(CMDCTL_BAD_CONFIG_DATA, errstr);
|
||||
return ccsession.create_answer(1, errstr)
|
||||
@@ -314,7 +314,7 @@ class CommandControl():
|
||||
self.modules_spec[args[0]] = args[1]
|
||||
|
||||
elif command == ccsession.COMMAND_SHUTDOWN:
|
||||
#When cmdctl get 'shutdown' command from boss,
|
||||
#When cmdctl get 'shutdown' command from boss,
|
||||
#shutdown the outer httpserver.
|
||||
self._httpserver.shutdown()
|
||||
self._serving = False
|
||||
@@ -384,12 +384,12 @@ class CommandControl():
|
||||
specs = self.get_modules_spec()
|
||||
if module_name not in specs.keys():
|
||||
return 1, {'error' : 'unknown module'}
|
||||
|
||||
|
||||
spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
|
||||
errors = []
|
||||
if not spec_obj.validate_command(command_name, params, errors):
|
||||
return 1, {'error': errors[0]}
|
||||
|
||||
|
||||
return self.send_command(module_name, command_name, params)
|
||||
|
||||
def send_command(self, module_name, command_name, params = None):
|
||||
@@ -400,7 +400,7 @@ class CommandControl():
|
||||
command_name, module_name)
|
||||
|
||||
if module_name == self._module_name:
|
||||
# Process the command sent to cmdctl directly.
|
||||
# Process the command sent to cmdctl directly.
|
||||
answer = self.command_handler(command_name, params)
|
||||
else:
|
||||
msg = ccsession.create_command(command_name, params)
|
||||
@@ -429,7 +429,7 @@ class CommandControl():
|
||||
|
||||
logger.error(CMDCTL_COMMAND_ERROR, command_name, module_name, errstr)
|
||||
return 1, {'error': errstr}
|
||||
|
||||
|
||||
def get_cmdctl_config_data(self):
|
||||
''' If running in source code tree, use keyfile, certificate
|
||||
and user accounts file in source code. '''
|
||||
@@ -453,13 +453,15 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
'''Make the server address can be reused.'''
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass,
|
||||
def __init__(self, server_address, RequestHandlerClass,
|
||||
CommandControlClass,
|
||||
idle_timeout = 1200, verbose = False):
|
||||
'''idle_timeout: the max idle time for login'''
|
||||
socketserver_mixin.NoPollMixIn.__init__(self)
|
||||
try:
|
||||
http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
|
||||
logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_STARTED,
|
||||
server_address[0], server_address[1])
|
||||
except socket.error as err:
|
||||
raise CmdctlException("Error creating server, because: %s \n" % str(err))
|
||||
|
||||
@@ -472,9 +474,9 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
self._accounts_file = None
|
||||
|
||||
def _create_user_info(self, accounts_file):
|
||||
'''Read all user's name and its' salt, hashed password
|
||||
'''Read all user's name and its' salt, hashed password
|
||||
from accounts file.'''
|
||||
if (self._accounts_file == accounts_file) and (len(self._user_infos) > 0):
|
||||
if (self._accounts_file == accounts_file) and (len(self._user_infos) > 0):
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
@@ -495,10 +497,10 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
self._accounts_file = accounts_file
|
||||
if len(self._user_infos) == 0:
|
||||
logger.error(CMDCTL_NO_USER_ENTRIES_READ)
|
||||
|
||||
|
||||
def get_user_info(self, username):
|
||||
'''Get user's salt and hashed string. If the user
|
||||
doesn't exist, return None, or else, the list
|
||||
doesn't exist, return None, or else, the list
|
||||
[salt, hashed password] will be returned.'''
|
||||
with self._lock:
|
||||
info = self._user_infos.get(username)
|
||||
@@ -507,9 +509,9 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
def save_user_session_id(self, session_id):
|
||||
''' Record user's id and login time. '''
|
||||
self.user_sessions[session_id] = time.time()
|
||||
|
||||
|
||||
def _check_key_and_cert(self, key, cert):
|
||||
# TODO, check the content of key/certificate file
|
||||
# TODO, check the content of key/certificate file
|
||||
if not os.path.exists(key):
|
||||
raise CmdctlException("key file '%s' doesn't exist " % key)
|
||||
|
||||
@@ -524,7 +526,7 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
certfile = cert,
|
||||
keyfile = key,
|
||||
ssl_version = ssl.PROTOCOL_SSLv23)
|
||||
return ssl_sock
|
||||
return ssl_sock
|
||||
except (ssl.SSLError, CmdctlException) as err :
|
||||
logger.info(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
|
||||
self.close_request(sock)
|
||||
@@ -541,18 +543,18 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
|
||||
|
||||
def get_reply_data_for_GET(self, id, module):
|
||||
'''Currently only support the following three url GET request '''
|
||||
rcode, reply = http.client.NO_CONTENT, []
|
||||
rcode, reply = http.client.NO_CONTENT, []
|
||||
if not module:
|
||||
if id == CONFIG_DATA_URL:
|
||||
rcode, reply = http.client.OK, self.cmdctl.get_config_data()
|
||||
elif id == MODULE_SPEC_URL:
|
||||
rcode, reply = http.client.OK, self.cmdctl.get_modules_spec()
|
||||
|
||||
return rcode, reply
|
||||
|
||||
return rcode, reply
|
||||
|
||||
def send_command_to_module(self, module_name, command_name, params):
|
||||
return self.cmdctl.send_command_with_check(module_name, command_name, params)
|
||||
|
||||
|
||||
httpd = None
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
@@ -566,10 +568,9 @@ def set_signal_handler():
|
||||
|
||||
def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
|
||||
''' Start cmdctl as one https server. '''
|
||||
if verbose:
|
||||
sys.stdout.write("[b10-cmdctl] starting on %s port:%d\n" %(addr, port))
|
||||
httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
|
||||
httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
|
||||
CommandControl, idle_timeout, verbose)
|
||||
|
||||
httpd.serve_forever()
|
||||
|
||||
def check_port(option, opt_str, value, parser):
|
||||
@@ -607,6 +608,8 @@ if __name__ == '__main__':
|
||||
(options, args) = parser.parse_args()
|
||||
result = 1 # in case of failure
|
||||
try:
|
||||
if options.verbose:
|
||||
logger.set_severity("DEBUG", 99)
|
||||
run(options.addr, options.port, options.idle_timeout, options.verbose)
|
||||
result = 0
|
||||
except isc.cc.SessionError as err:
|
||||
|
@@ -64,6 +64,9 @@ be set up. The specific error is given in the log message. Possible
|
||||
causes may be that the ssl request itself was bad, or the local key or
|
||||
certificate file could not be read.
|
||||
|
||||
% CMDCTL_STARTED cmdctl is listening for connections on %1:%2
|
||||
The cmdctl daemon has started and is now listening for connections.
|
||||
|
||||
% CMDCTL_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
|
||||
There was a keyboard interrupt signal to stop the cmdctl daemon. The
|
||||
daemon will now shut down.
|
||||
|
@@ -18,6 +18,7 @@ import struct
|
||||
import os
|
||||
import copy
|
||||
import subprocess
|
||||
import copy
|
||||
from isc.log_messages.bind10_messages import *
|
||||
from libutil_io_python import recv_fd
|
||||
|
||||
|
@@ -117,12 +117,13 @@ class ConfigManagerData:
|
||||
if file:
|
||||
file.close();
|
||||
return config
|
||||
|
||||
|
||||
def write_to_file(self, output_file_name = None):
|
||||
"""Writes the current configuration data to a file. If
|
||||
output_file_name is not specified, the file used in
|
||||
read_from_file is used."""
|
||||
filename = None
|
||||
|
||||
try:
|
||||
file = tempfile.NamedTemporaryFile(mode='w',
|
||||
prefix="b10-config.db.",
|
||||
@@ -291,7 +292,7 @@ class ConfigManager:
|
||||
# ok, just start with an empty config
|
||||
self.config = ConfigManagerData(self.data_path,
|
||||
self.database_filename)
|
||||
|
||||
|
||||
def write_config(self):
|
||||
"""Write the current configuration to the file specificied at init()"""
|
||||
self.config.write_to_file()
|
||||
@@ -445,7 +446,7 @@ class ConfigManager:
|
||||
answer = ccsession.create_answer(1, "Wrong number of arguments")
|
||||
if not answer:
|
||||
answer = ccsession.create_answer(1, "No answer message from " + cmd[0])
|
||||
|
||||
|
||||
return answer
|
||||
|
||||
def _handle_module_spec(self, spec):
|
||||
@@ -455,7 +456,7 @@ class ConfigManager:
|
||||
# todo: error checking (like keyerrors)
|
||||
answer = {}
|
||||
self.set_module_spec(spec)
|
||||
|
||||
|
||||
# We should make one general 'spec update for module' that
|
||||
# passes both specification and commands at once
|
||||
spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
|
||||
@@ -491,7 +492,7 @@ class ConfigManager:
|
||||
else:
|
||||
answer = ccsession.create_answer(1, "Unknown message format: " + str(msg))
|
||||
return answer
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Runs the configuration manager."""
|
||||
self.running = True
|
||||
|
@@ -37,7 +37,7 @@ class TestConfigManagerData(unittest.TestCase):
|
||||
It shouldn't append the data path to it.
|
||||
"""
|
||||
abs_path = self.data_path + os.sep + "b10-config-imaginary.db"
|
||||
data = ConfigManagerData(os.getcwd(), abs_path)
|
||||
data = ConfigManagerData(self.data_path, abs_path)
|
||||
self.assertEqual(abs_path, data.db_filename)
|
||||
self.assertEqual(self.data_path, data.data_path)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestConfigManagerData(unittest.TestCase):
|
||||
self.assertEqual(cfd1, cfd2)
|
||||
cfd2.data['test'] = { 'a': [ 1, 2, 3]}
|
||||
self.assertNotEqual(cfd1, cfd2)
|
||||
|
||||
|
||||
|
||||
class TestConfigManager(unittest.TestCase):
|
||||
|
||||
@@ -198,8 +198,8 @@ class TestConfigManager(unittest.TestCase):
|
||||
self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
|
||||
config_spec = self.cm.get_config_spec('Spec2')
|
||||
self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
|
||||
|
||||
|
||||
|
||||
|
||||
def test_get_commands_spec(self):
|
||||
commands_spec = self.cm.get_commands_spec()
|
||||
self.assertEqual(commands_spec, {})
|
||||
@@ -250,7 +250,7 @@ class TestConfigManager(unittest.TestCase):
|
||||
def test_write_config(self):
|
||||
# tested in ConfigManagerData tests
|
||||
pass
|
||||
|
||||
|
||||
def _handle_msg_helper(self, msg, expected_answer):
|
||||
answer = self.cm.handle_msg(msg)
|
||||
self.assertEqual(expected_answer, answer)
|
||||
@@ -338,7 +338,7 @@ class TestConfigManager(unittest.TestCase):
|
||||
# self.fake_session.get_message(self.name, None))
|
||||
#self.assertEqual({'version': 1, 'TestModule': {'test': 124}}, self.cm.config.data)
|
||||
#
|
||||
self._handle_msg_helper({ "command":
|
||||
self._handle_msg_helper({ "command":
|
||||
["module_spec", self.spec.get_full_spec()]
|
||||
},
|
||||
{'result': [0]})
|
||||
@@ -359,7 +359,7 @@ class TestConfigManager(unittest.TestCase):
|
||||
#self.assertEqual({'commands_update': [ self.name, self.commands ] },
|
||||
# self.fake_session.get_message("Cmdctl", None))
|
||||
|
||||
self._handle_msg_helper({ "command":
|
||||
self._handle_msg_helper({ "command":
|
||||
["shutdown"]
|
||||
},
|
||||
{'result': [0]})
|
||||
|
127
tests/lettuce/README
Normal file
127
tests/lettuce/README
Normal file
@@ -0,0 +1,127 @@
|
||||
BIND10 system testing with Lettuce
|
||||
or: to BDD or not to BDD
|
||||
|
||||
In this directory, we define a set of behavioral tests for BIND 10. Currently,
|
||||
these tests are specific for BIND10, but we are keeping in mind that RFC-related
|
||||
tests could be separated, so that we can test other systems as well.
|
||||
|
||||
Prerequisites:
|
||||
- Installed version of BIND 10 (but see below how to run it from source tree)
|
||||
- dig
|
||||
- lettuce (http://lettuce.it)
|
||||
|
||||
To install lettuce, if you have the python pip installation tool, simply do
|
||||
pip install lettuce
|
||||
See http://lettuce.it/intro/install.html
|
||||
|
||||
Most systems have the pip tool in a separate package; on Debian-based systems
|
||||
it is called python-pip. On FreeBSD the port is devel/py-pip.
|
||||
|
||||
Running the tests
|
||||
-----------------
|
||||
|
||||
At this moment, we have a fixed port for local tests in our setups, port 47806.
|
||||
This port must be free. (TODO: can we make this run-time discovered?).
|
||||
Port 47805 is used for cmdctl, and must also be available.
|
||||
(note, we will need to extend this to a range, or if possible, we will need to
|
||||
do some on-the-fly available port finding)
|
||||
|
||||
The bind10 main program, bindctl, and dig must all be in the default search
|
||||
path of your environment, and BIND 10 must not be running if you use the
|
||||
installed version when you run the tests.
|
||||
|
||||
If you want to test an installed version of bind 10, just run 'lettuce' in
|
||||
this directory.
|
||||
|
||||
We have provided a script that sets up the shell environment to run the tests
|
||||
with the build tree version of bind. If your shell uses export to set
|
||||
environment variables, you can source the script setup_intree_bind10.sh, then
|
||||
run lettuce.
|
||||
|
||||
Due to the default way lettuce prints its output, it is advisable to run it
|
||||
in a terminal that is wide than the default. If you see a lot of lines twice
|
||||
in different colors, the terminal is not wide enough.
|
||||
|
||||
If you just want to run one specific feature test, use
|
||||
lettuce features/<feature file>
|
||||
|
||||
To run a specific scenario from a feature, use
|
||||
lettuce features/<feature file> -s <scenario number>
|
||||
|
||||
We have set up the tests to assume that lettuce is run from this directory,
|
||||
so even if you specify a specific feature file, you should do it from this
|
||||
directory.
|
||||
|
||||
What to do when a test fails
|
||||
----------------------------
|
||||
|
||||
First of all, look at the error it printed and see what step it occurred in.
|
||||
If written well, the output should explain most of what went wrong.
|
||||
|
||||
The stacktrace that is printed is *not* of bind10, but of the testing
|
||||
framework; this helps in finding more information about what exactly the test
|
||||
tried to achieve when it failed (as well as help debug the tests themselves).
|
||||
|
||||
Furthermore, if any scenario fails, the output from long-running processes
|
||||
will be stored in the directory output/. The name of the files will be
|
||||
<Feature name>-<Scenario name>-<Process name>.stdout and
|
||||
<Feature name>-<Scenario name>-<Process name>.stderr
|
||||
Where spaces and other non-standard characters are replaced by an underscore.
|
||||
The process name is either the standard name for said process (e.g. 'bind10'),
|
||||
or the name given to it by the test ('when i run bind10 as <name>').
|
||||
|
||||
These files *will* be overwritten or deleted if the same scenarios are run
|
||||
again, so if you want to inspect them after a failed test, either do so
|
||||
immediately or move the files.
|
||||
|
||||
Adding and extending tests
|
||||
--------------------------
|
||||
|
||||
If you want to add tests, it is advisable to first go through the examples to
|
||||
see what is possible, and read the documentation on http://www.lettuce.it
|
||||
|
||||
There is also a README.tutorial file here.
|
||||
|
||||
We have a couple of conventions to keep things manageable.
|
||||
|
||||
Configuration files go into the configurations/ directory.
|
||||
Data files go into the data/ directory.
|
||||
Step definition go into the features/terrain/ directory (the name terrain is
|
||||
chosen for the same reason Lettuce chose terrain.py, this is the place the
|
||||
tests 'live' in).
|
||||
Feature definitions go directly into the features/ directory.
|
||||
|
||||
These directories are currently not divided further; we may want to consider
|
||||
this as the set grows. Due to a (current?) limitation of Lettuce, for
|
||||
feature files this is currently not possible; the python files containing
|
||||
steps and terrain must be below or at the same level of the feature files.
|
||||
|
||||
Long-running processes should be started through the world.RunningProcesses
|
||||
instance. If you want to add a process (e.g. bind9), create start, stop and
|
||||
control steps in terrain/<base_name>_control.py, and let it use the
|
||||
RunningProcesses API (defined in terrain.py). See bind10_control.py for an
|
||||
example.
|
||||
|
||||
For sending queries and checking the results, steps have been defined in
|
||||
terrain/querying.py. These use dig and store the results split up into text
|
||||
strings. This is intentionally not parsed through our own library (as that way
|
||||
we might run into a 'symmetric bug'). If you need something more advanced from
|
||||
query results, define it here.
|
||||
|
||||
Some very general steps are defined in terrain/steps.py.
|
||||
Initialization code, cleanup code, and helper classes are defined in
|
||||
terrain/terrain.py.
|
||||
|
||||
To find the right steps, case insensitive matching is used. Parameters taken
|
||||
from the steps are case-sensitive though. So a step defined as
|
||||
'do foo with value (bar)' will be matched when using
|
||||
'Do Foo with value xyz', but xyz will be taken as given.
|
||||
|
||||
If you need to add steps that are very particular to one test, create a new
|
||||
file with a name relevant for that test in terrain. We may want to consider
|
||||
creating a specific subdirectory for these, but at this moment it is unclear
|
||||
whether we need to.
|
||||
|
||||
We should try to keep steps as general as possible, while not making them to
|
||||
complex and error-prone.
|
||||
|
157
tests/lettuce/README.tutorial
Normal file
157
tests/lettuce/README.tutorial
Normal file
@@ -0,0 +1,157 @@
|
||||
Quick tutorial and overview
|
||||
---------------------------
|
||||
|
||||
Lettuce is a framework for doing Behaviour Driven Development (BDD).
|
||||
|
||||
The idea behind BDD is that you first write down your requirements in
|
||||
the form of scenarios, then implement their behaviour.
|
||||
|
||||
We do not plan on doing full BDD, but such a system should also help
|
||||
us make system tests. And, hopefully, being able to better identify
|
||||
what exactly is going wrong when a test fails.
|
||||
|
||||
Lettuce is a python implementation of the Cucumber framework, which is
|
||||
a ruby system. So far we chose lettuce because we already need python
|
||||
anyway, so chances are higher that any system we want to run it on
|
||||
supports it. It only supports a subset of cucumber, but more cucumber
|
||||
features are planned. As I do not know much details of cucumber, I
|
||||
can't really say what is there and what is not.
|
||||
|
||||
A slight letdown is that the current version does not support python 3.
|
||||
However, as long as the tool-calling glue is python2, this should not
|
||||
cause any problems, since these aren't unit tests; We do not plan to use
|
||||
our libraries directly, but only through the runnable scripts and
|
||||
executables.
|
||||
|
||||
-----
|
||||
|
||||
Features, Scenarios, Steps.
|
||||
|
||||
Lettuce makes a distinction between features, scenarios, and steps.
|
||||
|
||||
Features are general, well, features. Each 'feature' has its own file
|
||||
ending in .feature. A feature file contains a description and a number
|
||||
of scenarios. Each scenario tests one or more particular parts of the
|
||||
feature. Each scenario consists of a number of steps.
|
||||
|
||||
So let's open up a simple one.
|
||||
|
||||
-- example.feature
|
||||
Feature: showing off BIND 10
|
||||
This is to show BIND 10 running and that it answer queries
|
||||
|
||||
Scenario: Starting bind10
|
||||
# steps go here
|
||||
--
|
||||
|
||||
I have predefined a number of steps we can use, as we build test we
|
||||
will need to expand these, but we will look at them shortly.
|
||||
|
||||
This file defines a feature, just under the feature name we can
|
||||
provide a description of the feature.
|
||||
|
||||
The one scenario we have no has no steps, so if we run it we should
|
||||
see something like:
|
||||
|
||||
-- output
|
||||
> lettuce
|
||||
Feature: showing off BIND 10
|
||||
This is to show BIND 10 running and that it answer queries
|
||||
|
||||
Scenario: Starting bind10
|
||||
|
||||
1 feature (1 passed)
|
||||
1 scenario (1 passed)
|
||||
0 step (0 passed)
|
||||
--
|
||||
|
||||
Let's first add some steps that send queries.
|
||||
|
||||
--
|
||||
A query for www.example.com should have rcode REFUSED
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
--
|
||||
|
||||
Since we didn't start any bind10, dig will time out and the result
|
||||
should be an error saying it got no answer. Errors are in the
|
||||
form of stack traces (trigger by failed assertions), so we can find
|
||||
out easily where in the tests they occurred. Especially when the total
|
||||
set of steps gets bigger we might need that.
|
||||
|
||||
So let's add a step that starts bind10.
|
||||
|
||||
--
|
||||
When I start bind10 with configuration example.org.config
|
||||
--
|
||||
|
||||
This is not good enough; it will fire of the process, but setting up
|
||||
b10-auth may take a few moments, so we need to add a step to wait for
|
||||
it to be started before we continue.
|
||||
|
||||
--
|
||||
Then wait for bind10 auth to start
|
||||
--
|
||||
|
||||
And let's run the tests again.
|
||||
|
||||
--
|
||||
> lettuce
|
||||
|
||||
Feature: showing off BIND 10
|
||||
This is to show BIND 10 running and that it answer queries
|
||||
|
||||
Scenario: Starting bind10
|
||||
When I start bind10 with configuration example.org.config
|
||||
Then wait for bind10 auth to start
|
||||
A query for www.example.com should have rcode REFUSED
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
|
||||
1 feature (1 passed)
|
||||
1 scenario (1 passed)
|
||||
4 steps (4 passed)
|
||||
(finished within 2 seconds)
|
||||
--
|
||||
|
||||
So take a look at one of those steps, let's pick the first one.
|
||||
|
||||
A step is defined through a python decorator, which in essence is a regular
|
||||
expression; lettuce searches through all defined steps to find one that
|
||||
matches. These are 'partial' matches (unless specified otherwise in the
|
||||
regular expression itself), so if the step is defined with "do foo bar", the
|
||||
scenario can add words for readability "When I do foo bar".
|
||||
|
||||
Each captured group will be passed as an argument to the function we define.
|
||||
For bind10, i defined a configuration file, a cmdctl port, and a process
|
||||
name. The first two should be self-evident, and the process name is an
|
||||
optional name we give it, should we want to address it in the rest of the
|
||||
tests. This is most useful if we want to start multiple instances. In the
|
||||
next step (the wait for auth to start), I added a 'of <instance>'. So if we
|
||||
define the bind10 'as b10_second_instance', we can specify that one here as
|
||||
'of b10_second_instance'.
|
||||
|
||||
--
|
||||
When I start bind10 with configuration second.config
|
||||
with cmdctl port 12345 as b10_second_instance
|
||||
--
|
||||
(line wrapped for readability)
|
||||
|
||||
But notice how we needed two steps, which we probably always need (but
|
||||
not entirely always)? We can also combine steps; for instance:
|
||||
|
||||
--
|
||||
@step('have bind10 running(?: with configuration ([\w.]+))?')
|
||||
def have_bind10_running(step, config_file):
|
||||
step.given('start bind10 with configuration ' + config_file)
|
||||
step.given('wait for bind10 auth to start')
|
||||
--
|
||||
|
||||
Now we can replace the two steps with one:
|
||||
|
||||
--
|
||||
Given I have bind10 running
|
||||
--
|
||||
|
||||
That's it for the quick overview. For some more examples, with comments,
|
||||
take a look at features/example.feature. You can read more about lettuce and
|
||||
its features on http://www.lettuce.it, and if you plan on adding tests and
|
||||
scenarios, please consult the last section of the main README first.
|
17
tests/lettuce/configurations/example.org.config.orig
Normal file
17
tests/lettuce/configurations/example.org.config.orig
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 2,
|
||||
"Logging": {
|
||||
"loggers": [ {
|
||||
"debuglevel": 99,
|
||||
"severity": "DEBUG",
|
||||
"name": "auth"
|
||||
} ]
|
||||
},
|
||||
"Auth": {
|
||||
"database_file": "data/example.org.sqlite3",
|
||||
"listen_on": [ {
|
||||
"port": 47806,
|
||||
"address": "127.0.0.1"
|
||||
} ]
|
||||
}
|
||||
}
|
18
tests/lettuce/configurations/example2.org.config
Normal file
18
tests/lettuce/configurations/example2.org.config
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 2,
|
||||
"Logging": {
|
||||
"loggers": [ {
|
||||
"severity": "DEBUG",
|
||||
"name": "auth",
|
||||
"debuglevel": 99
|
||||
}
|
||||
]
|
||||
},
|
||||
"Auth": {
|
||||
"database_file": "data/example.org.sqlite3",
|
||||
"listen_on": [ {
|
||||
"port": 47807,
|
||||
"address": "127.0.0.1"
|
||||
} ]
|
||||
}
|
||||
}
|
10
tests/lettuce/configurations/no_db_file.config
Normal file
10
tests/lettuce/configurations/no_db_file.config
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"Auth": {
|
||||
"database_file": "data/test_nonexistent_db.sqlite3",
|
||||
"listen_on": [ {
|
||||
"port": 47806,
|
||||
"address": "127.0.0.1"
|
||||
} ]
|
||||
}
|
||||
}
|
BIN
tests/lettuce/data/empty_db.sqlite3
Normal file
BIN
tests/lettuce/data/empty_db.sqlite3
Normal file
Binary file not shown.
BIN
tests/lettuce/data/example.org.sqlite3
Normal file
BIN
tests/lettuce/data/example.org.sqlite3
Normal file
Binary file not shown.
142
tests/lettuce/features/example.feature
Normal file
142
tests/lettuce/features/example.feature
Normal file
@@ -0,0 +1,142 @@
|
||||
Feature: Example feature
|
||||
This is an example Feature set. Is is mainly intended to show
|
||||
our use of the lettuce tool and our own framework for it
|
||||
The first scenario is to show what a simple test would look like, and
|
||||
is intentionally uncommented.
|
||||
The later scenarios have comments to show what the test steps do and
|
||||
support
|
||||
|
||||
Scenario: A simple example
|
||||
Given I have bind10 running with configuration example.org.config
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
A query for www.doesnotexist.org should have rcode REFUSED
|
||||
The SOA serial for example.org should be 1234
|
||||
|
||||
Scenario: New database
|
||||
# This test checks whether a database file is automatically created
|
||||
# Underwater, we take advantage of our intialization routines so
|
||||
# that we are sure this file does not exist, see
|
||||
# features/terrain/terrain.py
|
||||
|
||||
# Standard check to test (non-)existence of a file
|
||||
# This file is actually automatically
|
||||
The file data/test_nonexistent_db.sqlite3 should not exist
|
||||
|
||||
# In the first scenario, we used 'given I have bind10 running', which
|
||||
# is actually a compound step consisting of the following two
|
||||
# one to start the server
|
||||
When I start bind10 with configuration no_db_file.config
|
||||
# And one to wait until it reports that b10-auth has started
|
||||
Then wait for bind10 auth to start
|
||||
|
||||
# This is a general step to stop a named process. By convention,
|
||||
# the default name for any process is the same as the one we
|
||||
# use in the start step (for bind 10, that is 'I start bind10 with')
|
||||
# See scenario 'Multiple instances' for more.
|
||||
Then stop process bind10
|
||||
|
||||
# Now we use the first step again to see if the file has been created
|
||||
The file data/test_nonexistent_db.sqlite3 should exist
|
||||
|
||||
Scenario: example.org queries
|
||||
# This scenario performs a number of queries and inspects the results
|
||||
# Simple queries have already been show, but after we have sent a query,
|
||||
# we can also do more extensive checks on the result.
|
||||
# See querying.py for more information on these steps.
|
||||
|
||||
# note: lettuce can group similar checks by using tables, but we
|
||||
# intentionally do not make use of that here
|
||||
|
||||
# This is a compound statement that starts and waits for the
|
||||
# started message
|
||||
Given I have bind10 running with configuration example.org.config
|
||||
|
||||
# Some simple queries that is not examined further
|
||||
A query for www.example.com should have rcode REFUSED
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
|
||||
# A query where we look at some of the result properties
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
The last query response should have qdcount 1
|
||||
The last query response should have ancount 1
|
||||
The last query response should have nscount 3
|
||||
The last query response should have adcount 0
|
||||
# The answer section can be inspected in its entirety; in the future
|
||||
# we may add more granular inspection steps
|
||||
The answer section of the last query response should be
|
||||
"""
|
||||
www.example.org. 3600 IN A 192.0.2.1
|
||||
"""
|
||||
|
||||
A query for example.org type NS should have rcode NOERROR
|
||||
The answer section of the last query response should be
|
||||
"""
|
||||
example.org. 3600 IN NS ns1.example.org.
|
||||
example.org. 3600 IN NS ns2.example.org.
|
||||
example.org. 3600 IN NS ns3.example.org.
|
||||
"""
|
||||
|
||||
# We have a specific step for checking SOA serial numbers
|
||||
The SOA serial for example.org should be 1234
|
||||
|
||||
# Another query where we look at some of the result properties
|
||||
A query for doesnotexist.example.org should have rcode NXDOMAIN
|
||||
The last query response should have qdcount 1
|
||||
The last query response should have ancount 0
|
||||
The last query response should have nscount 1
|
||||
The last query response should have adcount 0
|
||||
# When checking flags, we must pass them exactly as they appear in
|
||||
# the output of dig.
|
||||
The last query response should have flags qr aa rd
|
||||
|
||||
A query for www.example.org type TXT should have rcode NOERROR
|
||||
The last query response should have ancount 0
|
||||
|
||||
# Some queries where we specify more details about what to send and
|
||||
# where
|
||||
A query for www.example.org class CH should have rcode REFUSED
|
||||
A query for www.example.org to 127.0.0.1 should have rcode NOERROR
|
||||
A query for www.example.org to 127.0.0.1:47806 should have rcode NOERROR
|
||||
A query for www.example.org type A class IN to 127.0.0.1:47806 should have rcode NOERROR
|
||||
|
||||
Scenario: changing database
|
||||
# This scenario contains a lot of 'wait for' steps
|
||||
# If those are not present, the asynchronous nature of the application
|
||||
# can cause some of the things we send to be handled out of order;
|
||||
# for instance auth could still be serving the old zone when we send
|
||||
# the new query, or already respond from the new database.
|
||||
# Therefore we wait for specific log messages after each operation
|
||||
#
|
||||
# This scenario outlines every single step, and does not use
|
||||
# 'steps of steps' (e.g. Given I have bind10 running)
|
||||
# We can do that but as an example this is probably better to learn
|
||||
# the system
|
||||
|
||||
When I start bind10 with configuration example.org.config
|
||||
Then wait for bind10 auth to start
|
||||
Wait for bind10 stderr message CMDCTL_STARTED
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
Wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
|
||||
Then set bind10 configuration Auth/database_file to data/empty_db.sqlite3
|
||||
And wait for new bind10 stderr message DATASRC_SQLITE_OPEN
|
||||
A query for www.example.org should have rcode REFUSED
|
||||
Wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
|
||||
Then set bind10 configuration Auth/database_file to data/example.org.sqlite3
|
||||
And wait for new bind10 stderr message DATASRC_SQLITE_OPEN
|
||||
A query for www.example.org should have rcode NOERROR
|
||||
|
||||
Scenario: two bind10 instances
|
||||
# This is more a test of the test system, start 2 bind10's
|
||||
When I start bind10 with configuration example.org.config as bind10_one
|
||||
And I start bind10 with configuration example2.org.config with cmdctl port 47804 as bind10_two
|
||||
|
||||
Then wait for bind10 auth of bind10_one to start
|
||||
Then wait for bind10 auth of bind10_two to start
|
||||
A query for www.example.org to 127.0.0.1:47806 should have rcode NOERROR
|
||||
A query for www.example.org to 127.0.0.1:47807 should have rcode NOERROR
|
||||
|
||||
Then set bind10 configuration Auth/database_file to data/empty_db.sqlite3
|
||||
And wait for bind10_one stderr message DATASRC_SQLITE_OPEN
|
||||
|
||||
A query for www.example.org to 127.0.0.1:47806 should have rcode REFUSED
|
||||
A query for www.example.org to 127.0.0.1:47807 should have rcode NOERROR
|
108
tests/lettuce/features/terrain/bind10_control.py
Normal file
108
tests/lettuce/features/terrain/bind10_control.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# 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.
|
||||
|
||||
from lettuce import *
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
@step('start bind10(?: with configuration (\S+))?' +\
|
||||
'(?: with cmdctl port (\d+))?(?: as (\S+))?')
|
||||
def start_bind10(step, config_file, cmdctl_port, process_name):
|
||||
"""
|
||||
Start BIND 10 with the given optional config file, cmdctl port, and
|
||||
store the running process in world with the given process name.
|
||||
Parameters:
|
||||
config_file ('with configuration <file>', optional): this configuration
|
||||
will be used. The path is relative to the base lettuce
|
||||
directory.
|
||||
cmdctl_port ('with cmdctl port <portnr>', optional): The port on which
|
||||
b10-cmdctl listens for bindctl commands. Defaults to 47805.
|
||||
process_name ('as <name>', optional). This is the name that can be used
|
||||
in the following steps of the scenario to refer to this
|
||||
BIND 10 instance. Defaults to 'bind10'.
|
||||
This call will block until BIND10_STARTUP_COMPLETE or BIND10_STARTUP_ERROR
|
||||
is logged. In the case of the latter, or if it times out, the step (and
|
||||
scenario) will fail.
|
||||
It will also fail if there is a running process with the given process_name
|
||||
already.
|
||||
"""
|
||||
args = [ 'bind10', '-v' ]
|
||||
if config_file is not None:
|
||||
args.append('-p')
|
||||
args.append("configurations/")
|
||||
args.append('-c')
|
||||
args.append(config_file)
|
||||
if cmdctl_port is None:
|
||||
args.append('--cmdctl-port=47805')
|
||||
else:
|
||||
args.append('--cmdctl-port=' + cmdctl_port)
|
||||
if process_name is None:
|
||||
process_name = "bind10"
|
||||
else:
|
||||
args.append('-m')
|
||||
args.append(process_name + '_msgq.socket')
|
||||
|
||||
world.processes.add_process(step, process_name, args)
|
||||
|
||||
# check output to know when startup has been completed
|
||||
message = world.processes.wait_for_stderr_str(process_name,
|
||||
["BIND10_STARTUP_COMPLETE",
|
||||
"BIND10_STARTUP_ERROR"])
|
||||
assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(message)
|
||||
|
||||
@step('wait for bind10 auth (?:of (\w+) )?to start')
|
||||
def wait_for_auth(step, process_name):
|
||||
"""Wait for b10-auth to run. This is done by blocking until the message
|
||||
AUTH_SERVER_STARTED is logged.
|
||||
Parameters:
|
||||
process_name ('of <name', optional): The name of the BIND 10 instance
|
||||
to wait for. Defaults to 'bind10'.
|
||||
"""
|
||||
if process_name is None:
|
||||
process_name = "bind10"
|
||||
world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'],
|
||||
False)
|
||||
|
||||
@step('have bind10 running(?: with configuration ([\w.]+))?')
|
||||
def have_bind10_running(step, config_file):
|
||||
"""
|
||||
Compound convenience step for running bind10, which consists of
|
||||
start_bind10 and wait_for_auth.
|
||||
Currently only supports the 'with configuration' option.
|
||||
"""
|
||||
step.given('start bind10 with configuration ' + config_file)
|
||||
step.given('wait for bind10 auth to start')
|
||||
|
||||
@step('set bind10 configuration (\S+) to (.*)(?: with cmdctl port (\d+))?')
|
||||
def set_config_command(step, name, value, cmdctl_port):
|
||||
"""
|
||||
Run bindctl, set the given configuration to the given value, and commit it.
|
||||
Parameters:
|
||||
name ('configuration <name>'): Identifier of the configuration to set
|
||||
value ('to <value>'): value to set it to.
|
||||
cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
|
||||
the command to. Defaults to 47805.
|
||||
Fails if cmdctl does not exit with status code 0.
|
||||
"""
|
||||
if cmdctl_port is None:
|
||||
cmdctl_port = '47805'
|
||||
args = ['bindctl', '-p', cmdctl_port]
|
||||
bindctl = subprocess.Popen(args, 1, None, subprocess.PIPE,
|
||||
subprocess.PIPE, None)
|
||||
bindctl.stdin.write("config set " + name + " " + value + "\n")
|
||||
bindctl.stdin.write("config commit\n")
|
||||
bindctl.stdin.write("quit\n")
|
||||
result = bindctl.wait()
|
||||
assert result == 0, "bindctl exit code: " + str(result)
|
279
tests/lettuce/features/terrain/querying.py
Normal file
279
tests/lettuce/features/terrain/querying.py
Normal file
@@ -0,0 +1,279 @@
|
||||
# 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 script provides querying functionality
|
||||
# The most important step is
|
||||
#
|
||||
# query for <name> [type X] [class X] [to <addr>[:port]] should have rcode <rc>
|
||||
#
|
||||
# By default, it will send queries to 127.0.0.1:47806 unless specified
|
||||
# otherwise. The rcode is always checked. If the result is not NO_ANSWER,
|
||||
# the result will be stored in last_query_result, which can then be inspected
|
||||
# more closely, for instance with the step
|
||||
#
|
||||
# "the last query response should have <property> <value>"
|
||||
#
|
||||
# Also see example.feature for some examples
|
||||
|
||||
from lettuce import *
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
#
|
||||
# define a class to easily access different parts
|
||||
# We may consider using our full library for this, but for now
|
||||
# simply store several parts of the response as text values in
|
||||
# this structure.
|
||||
# (this actually has the advantage of not relying on our own libraries
|
||||
# to test our own, well, libraries)
|
||||
#
|
||||
# The following attributes are 'parsed' from the response, all as strings,
|
||||
# and end up as direct attributes of the QueryResult object:
|
||||
# opcode, rcode, id, flags, qdcount, ancount, nscount, adcount
|
||||
# (flags is one string with all flags, in the order they appear in the
|
||||
# response packet.)
|
||||
#
|
||||
# this will set 'rcode' as the result code, we 'define' one additional
|
||||
# rcode, "NO_ANSWER", if the dig process returned an error code itself
|
||||
# In this case none of the other attributes will be set.
|
||||
#
|
||||
# The different sections will be lists of strings, one for each RR in the
|
||||
# section. The question section will start with ';', as per dig output
|
||||
#
|
||||
# See server_from_sqlite3.feature for various examples to perform queries
|
||||
class QueryResult(object):
|
||||
status_re = re.compile("opcode: ([A-Z])+, status: ([A-Z]+), id: ([0-9]+)")
|
||||
flags_re = re.compile("flags: ([a-z ]+); QUERY: ([0-9]+), ANSWER: " +
|
||||
"([0-9]+), AUTHORITY: ([0-9]+), ADDITIONAL: ([0-9]+)")
|
||||
|
||||
def __init__(self, name, qtype, qclass, address, port):
|
||||
"""
|
||||
Constructor. This fires of a query using dig.
|
||||
Parameters:
|
||||
name: The domain name to query
|
||||
qtype: The RR type to query. Defaults to A if it is None.
|
||||
qclass: The RR class to query. Defaults to IN if it is None.
|
||||
address: The IP adress to send the query to.
|
||||
port: The port number to send the query to.
|
||||
All parameters must be either strings or have the correct string
|
||||
representation.
|
||||
Only one query attempt will be made.
|
||||
"""
|
||||
args = [ 'dig', '+tries=1', '@' + str(address), '-p', str(port) ]
|
||||
if qtype is not None:
|
||||
args.append('-t')
|
||||
args.append(str(qtype))
|
||||
if qclass is not None:
|
||||
args.append('-c')
|
||||
args.append(str(qclass))
|
||||
args.append(name)
|
||||
dig_process = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
|
||||
None)
|
||||
result = dig_process.wait()
|
||||
if result != 0:
|
||||
self.rcode = "NO_ANSWER"
|
||||
else:
|
||||
self.rcode = None
|
||||
parsing = "HEADER"
|
||||
self.question_section = []
|
||||
self.answer_section = []
|
||||
self.authority_section = []
|
||||
self.additional_section = []
|
||||
self.line_handler = self.parse_header
|
||||
for out in dig_process.stdout:
|
||||
self.line_handler(out)
|
||||
|
||||
def _check_next_header(self, line):
|
||||
"""
|
||||
Returns true if we found a next header, and sets the internal
|
||||
line handler to the appropriate value.
|
||||
"""
|
||||
if line == ";; ANSWER SECTION:\n":
|
||||
self.line_handler = self.parse_answer
|
||||
elif line == ";; AUTHORITY SECTION:\n":
|
||||
self.line_handler = self.parse_authority
|
||||
elif line == ";; ADDITIONAL SECTION:\n":
|
||||
self.line_handler = self.parse_additional
|
||||
elif line.startswith(";; Query time"):
|
||||
self.line_handler = self.parse_footer
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_header(self, line):
|
||||
"""
|
||||
Parse the header lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
if not self._check_next_header(line):
|
||||
status_match = self.status_re.search(line)
|
||||
flags_match = self.flags_re.search(line)
|
||||
if status_match is not None:
|
||||
self.opcode = status_match.group(1)
|
||||
self.rcode = status_match.group(2)
|
||||
elif flags_match is not None:
|
||||
self.flags = flags_match.group(1)
|
||||
self.qdcount = flags_match.group(2)
|
||||
self.ancount = flags_match.group(3)
|
||||
self.nscount = flags_match.group(4)
|
||||
self.adcount = flags_match.group(5)
|
||||
|
||||
def parse_question(self, line):
|
||||
"""
|
||||
Parse the question section lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
if not self._check_next_header(line):
|
||||
if line != "\n":
|
||||
self.question_section.append(line.strip())
|
||||
|
||||
def parse_answer(self, line):
|
||||
"""
|
||||
Parse the answer section lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
if not self._check_next_header(line):
|
||||
if line != "\n":
|
||||
self.answer_section.append(line.strip())
|
||||
|
||||
def parse_authority(self, line):
|
||||
"""
|
||||
Parse the authority section lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
if not self._check_next_header(line):
|
||||
if line != "\n":
|
||||
self.authority_section.append(line.strip())
|
||||
|
||||
def parse_additional(self, line):
|
||||
"""
|
||||
Parse the additional section lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
if not self._check_next_header(line):
|
||||
if line != "\n":
|
||||
self.additional_section.append(line.strip())
|
||||
|
||||
def parse_footer(self, line):
|
||||
"""
|
||||
Parse the footer lines of the query response.
|
||||
Parameters:
|
||||
line: The current line of the response.
|
||||
"""
|
||||
pass
|
||||
|
||||
@step('A query for ([\w.]+) (?:type ([A-Z]+) )?(?:class ([A-Z]+) )?' +
|
||||
'(?:to ([^:]+)(?::([0-9]+))? )?should have rcode ([\w.]+)')
|
||||
def query(step, query_name, qtype, qclass, addr, port, rcode):
|
||||
"""
|
||||
Run a query, check the rcode of the response, and store the query
|
||||
result in world.last_query_result.
|
||||
Parameters:
|
||||
query_name ('query for <name>'): The domain name to query.
|
||||
qtype ('type <type>', optional): The RR type to query. Defaults to A.
|
||||
qclass ('class <class>', optional): The RR class to query. Defaults to IN.
|
||||
addr ('to <address>', optional): The IP address of the nameserver to query.
|
||||
Defaults to 127.0.0.1.
|
||||
port (':<port>', optional): The port number of the nameserver to query.
|
||||
Defaults to 47806.
|
||||
rcode ('should have rcode <rcode>'): The expected rcode of the answer.
|
||||
"""
|
||||
if qtype is None:
|
||||
qtype = "A"
|
||||
if qclass is None:
|
||||
qclass = "IN"
|
||||
if addr is None:
|
||||
addr = "127.0.0.1"
|
||||
if port is None:
|
||||
port = 47806
|
||||
query_result = QueryResult(query_name, qtype, qclass, addr, port)
|
||||
assert query_result.rcode == rcode,\
|
||||
"Expected: " + rcode + ", got " + query_result.rcode
|
||||
world.last_query_result = query_result
|
||||
|
||||
@step('The SOA serial for ([\w.]+) should be ([0-9]+)')
|
||||
def query_soa(step, query_name, serial):
|
||||
"""
|
||||
Convenience function to check the SOA SERIAL value of the given zone at
|
||||
the nameserver at the default address (127.0.0.1:47806).
|
||||
Parameters:
|
||||
query_name ('for <name>'): The zone to find the SOA record for.
|
||||
serial ('should be <number>'): The expected value of the SOA SERIAL.
|
||||
If the rcode is not NOERROR, or the answer section does not contain the
|
||||
SOA record, this step fails.
|
||||
"""
|
||||
query_result = QueryResult(query_name, "SOA", "IN", "127.0.0.1", "47806")
|
||||
assert "NOERROR" == query_result.rcode,\
|
||||
"Got " + query_result.rcode + ", expected NOERROR"
|
||||
assert len(query_result.answer_section) == 1,\
|
||||
"Too few or too many answers in SOA response"
|
||||
soa_parts = query_result.answer_section[0].split()
|
||||
assert serial == soa_parts[6],\
|
||||
"Got SOA serial " + soa_parts[6] + ", expected " + serial
|
||||
|
||||
@step('last query response should have (\S+) (.+)')
|
||||
def check_last_query(step, item, value):
|
||||
"""
|
||||
Check a specific value in the reponse from the last successful query sent.
|
||||
Parameters:
|
||||
item: The item to check the value of
|
||||
value: The expected value.
|
||||
This performs a very simple direct string comparison of the QueryResult
|
||||
member with the given item name and the given value.
|
||||
Fails if the item is unknown, or if its value does not match the expected
|
||||
value.
|
||||
"""
|
||||
assert world.last_query_result is not None
|
||||
assert item in world.last_query_result.__dict__
|
||||
lq_val = world.last_query_result.__dict__[item]
|
||||
assert str(value) == str(lq_val),\
|
||||
"Got: " + str(lq_val) + ", expected: " + str(value)
|
||||
|
||||
@step('([a-zA-Z]+) section of the last query response should be')
|
||||
def check_last_query_section(step, section):
|
||||
"""
|
||||
Check the entire contents of the given section of the response of the last
|
||||
query.
|
||||
Parameters:
|
||||
section ('<section> section'): The name of the section (QUESTION, ANSWER,
|
||||
AUTHORITY or ADDITIONAL).
|
||||
The expected response is taken from the multiline part of the step in the
|
||||
scenario. Differing whitespace is ignored, but currently the order is
|
||||
significant.
|
||||
Fails if they do not match.
|
||||
"""
|
||||
response_string = None
|
||||
if section.lower() == 'question':
|
||||
response_string = "\n".join(world.last_query_result.question_section)
|
||||
elif section.lower() == 'answer':
|
||||
response_string = "\n".join(world.last_query_result.answer_section)
|
||||
elif section.lower() == 'authority':
|
||||
response_string = "\n".join(world.last_query_result.answer_section)
|
||||
elif section.lower() == 'additional':
|
||||
response_string = "\n".join(world.last_query_result.answer_section)
|
||||
else:
|
||||
assert False, "Unknown section " + section
|
||||
# replace whitespace of any length by one space
|
||||
response_string = re.sub("[ \t]+", " ", response_string)
|
||||
expect = re.sub("[ \t]+", " ", step.multiline)
|
||||
assert response_string.strip() == expect.strip(),\
|
||||
"Got:\n'" + response_string + "'\nExpected:\n'" + step.multiline +"'"
|
||||
|
||||
|
73
tests/lettuce/features/terrain/steps.py
Normal file
73
tests/lettuce/features/terrain/steps.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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 file contains a number of common steps that are general and may be used
|
||||
# By a lot of feature files.
|
||||
#
|
||||
|
||||
from lettuce import *
|
||||
import os
|
||||
|
||||
@step('stop process (\w+)')
|
||||
def stop_a_named_process(step, process_name):
|
||||
"""
|
||||
Stop the process with the given name.
|
||||
Parameters:
|
||||
process_name ('process <name>'): Name of the process to stop.
|
||||
"""
|
||||
world.processes.stop_process(process_name)
|
||||
|
||||
@step('wait for (new )?(\w+) stderr message (\w+)')
|
||||
def wait_for_message(step, new, process_name, message):
|
||||
"""
|
||||
Block until the given message is printed to the given process's stderr
|
||||
output.
|
||||
Parameter:
|
||||
new: (' new', optional): Only check the output printed since last time
|
||||
this step was used for this process.
|
||||
process_name ('<name> stderr'): Name of the process to check the output of.
|
||||
message ('message <message>'): Output (part) to wait for.
|
||||
Fails if the message is not found after 10 seconds.
|
||||
"""
|
||||
world.processes.wait_for_stderr_str(process_name, [message], new)
|
||||
|
||||
@step('wait for (new )?(\w+) stdout message (\w+)')
|
||||
def wait_for_message(step, process_name, message):
|
||||
"""
|
||||
Block until the given message is printed to the given process's stdout
|
||||
output.
|
||||
Parameter:
|
||||
new: (' new', optional): Only check the output printed since last time
|
||||
this step was used for this process.
|
||||
process_name ('<name> stderr'): Name of the process to check the output of.
|
||||
message ('message <message>'): Output (part) to wait for.
|
||||
Fails if the message is not found after 10 seconds.
|
||||
"""
|
||||
world.processes.wait_for_stdout_str(process_name, [message], new)
|
||||
|
||||
@step('the file (\S+) should (not )?exist')
|
||||
def check_existence(step, file_name, should_not_exist):
|
||||
"""
|
||||
Check the existence of the given file.
|
||||
Parameters:
|
||||
file_name ('file <name>'): File to check existence of.
|
||||
should_not_exist ('not', optional): Whether it should or should not exist.
|
||||
Fails if the file should exist and does not, or vice versa.
|
||||
"""
|
||||
if should_not_exist is None:
|
||||
assert os.path.exists(file_name), file_name + " does not exist"
|
||||
else:
|
||||
assert not os.path.exists(file_name), file_name + " exists"
|
360
tests/lettuce/features/terrain/terrain.py
Normal file
360
tests/lettuce/features/terrain/terrain.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# 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 'terrain' in which the lettuce lives. By convention, this is
|
||||
# where global setup and teardown is defined.
|
||||
#
|
||||
# We declare some attributes of the global 'world' variables here, so the
|
||||
# tests can safely assume they are present.
|
||||
#
|
||||
# We also use it to provide scenario invariants, such as resetting data.
|
||||
#
|
||||
|
||||
from lettuce import *
|
||||
import subprocess
|
||||
import os.path
|
||||
import shutil
|
||||
import re
|
||||
import time
|
||||
|
||||
# In order to make sure we start all tests with a 'clean' environment,
|
||||
# We perform a number of initialization steps, like restoring configuration
|
||||
# files, and removing generated data files.
|
||||
|
||||
# This approach may not scale; if so we should probably provide specific
|
||||
# initialization steps for scenarios. But until that is shown to be a problem,
|
||||
# It will keep the scenarios cleaner.
|
||||
|
||||
# This is a list of files that are freshly copied before each scenario
|
||||
# The first element is the original, the second is the target that will be
|
||||
# used by the tests that need them
|
||||
copylist = [
|
||||
["configurations/example.org.config.orig", "configurations/example.org.config"]
|
||||
]
|
||||
|
||||
# This is a list of files that, if present, will be removed before a scenario
|
||||
removelist = [
|
||||
"data/test_nonexistent_db.sqlite3"
|
||||
]
|
||||
|
||||
# When waiting for output data of a running process, use OUTPUT_WAIT_INTERVAL
|
||||
# as the interval in which to check again if it has not been found yet.
|
||||
# If we have waited OUTPUT_WAIT_MAX_INTERVALS times, we will abort with an
|
||||
# error (so as not to hang indefinitely)
|
||||
OUTPUT_WAIT_INTERVAL = 0.5
|
||||
OUTPUT_WAIT_MAX_INTERVALS = 20
|
||||
|
||||
# class that keeps track of one running process and the files
|
||||
# we created for it.
|
||||
class RunningProcess:
|
||||
def __init__(self, step, process_name, args):
|
||||
# set it to none first so destructor won't error if initializer did
|
||||
"""
|
||||
Initialize the long-running process structure, and start the process.
|
||||
Parameters:
|
||||
step: The scenario step it was called from. This is used for
|
||||
determining the output files for redirection of stdout
|
||||
and stderr.
|
||||
process_name: The name to refer to this running process later.
|
||||
args: Array of arguments to pass to Popen().
|
||||
"""
|
||||
self.process = None
|
||||
self.step = step
|
||||
self.process_name = process_name
|
||||
self.remove_files_on_exit = True
|
||||
self._check_output_dir()
|
||||
self._create_filenames()
|
||||
self._start_process(args)
|
||||
|
||||
def _start_process(self, args):
|
||||
"""
|
||||
Start the process.
|
||||
Parameters:
|
||||
args:
|
||||
Array of arguments to pass to Popen().
|
||||
"""
|
||||
stderr_write = open(self.stderr_filename, "w")
|
||||
stdout_write = open(self.stdout_filename, "w")
|
||||
self.process = subprocess.Popen(args, 1, None, subprocess.PIPE,
|
||||
stdout_write, stderr_write)
|
||||
# open them again, this time for reading
|
||||
self.stderr = open(self.stderr_filename, "r")
|
||||
self.stdout = open(self.stdout_filename, "r")
|
||||
|
||||
def mangle_filename(self, filebase, extension):
|
||||
"""
|
||||
Remove whitespace and non-default characters from a base string,
|
||||
and return the substituted value. Whitespace is replaced by an
|
||||
underscore. Any other character that is not an ASCII letter, a
|
||||
number, a dot, or a hyphen or underscore is removed.
|
||||
Parameter:
|
||||
filebase: The string to perform the substitution and removal on
|
||||
extension: An extension to append to the result value
|
||||
Returns the modified filebase with the given extension
|
||||
"""
|
||||
filebase = re.sub("\s+", "_", filebase)
|
||||
filebase = re.sub("[^a-zA-Z0-9.\-_]", "", filebase)
|
||||
return filebase + "." + extension
|
||||
|
||||
def _check_output_dir(self):
|
||||
# We may want to make this overridable by the user, perhaps
|
||||
# through an environment variable. Since we currently expect
|
||||
# lettuce to be run from our lettuce dir, we shall just use
|
||||
# the relative path 'output/'
|
||||
"""
|
||||
Make sure the output directory for stdout/stderr redirection
|
||||
exists.
|
||||
Fails if it exists but is not a directory, or if it does not
|
||||
and we are unable to create it.
|
||||
"""
|
||||
self._output_dir = os.getcwd() + os.sep + "output"
|
||||
if not os.path.exists(self._output_dir):
|
||||
os.mkdir(self._output_dir)
|
||||
assert os.path.isdir(self._output_dir),\
|
||||
self._output_dir + " is not a directory."
|
||||
|
||||
def _create_filenames(self):
|
||||
"""
|
||||
Derive the filenames for stdout/stderr redirection from the
|
||||
feature, scenario, and process name. The base will be
|
||||
"<Feature>-<Scenario>-<process name>.[stdout|stderr]"
|
||||
"""
|
||||
filebase = self.step.scenario.feature.name + "-" +\
|
||||
self.step.scenario.name + "-" + self.process_name
|
||||
self.stderr_filename = self._output_dir + os.sep +\
|
||||
self.mangle_filename(filebase, "stderr")
|
||||
self.stdout_filename = self._output_dir + os.sep +\
|
||||
self.mangle_filename(filebase, "stdout")
|
||||
|
||||
def stop_process(self):
|
||||
"""
|
||||
Stop this process by calling terminate(). Blocks until process has
|
||||
exited. If remove_files_on_exit is True, redirected output files
|
||||
are removed.
|
||||
"""
|
||||
if self.process is not None:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
if self.remove_files_on_exit:
|
||||
self._remove_files()
|
||||
|
||||
def _remove_files(self):
|
||||
"""
|
||||
Remove the files created for redirection of stdout/stderr output.
|
||||
"""
|
||||
os.remove(self.stderr_filename)
|
||||
os.remove(self.stdout_filename)
|
||||
|
||||
def _wait_for_output_str(self, filename, running_file, strings, only_new):
|
||||
"""
|
||||
Wait for a line of output in this process. This will (if only_new is
|
||||
False) first check all previous output from the process, and if not
|
||||
found, check all output since the last time this method was called.
|
||||
For each line in the output, the given strings array is checked. If
|
||||
any output lines checked contains one of the strings in the strings
|
||||
array, that string (not the line!) is returned.
|
||||
Parameters:
|
||||
filename: The filename to read previous output from, if applicable.
|
||||
running_file: The open file to read new output from.
|
||||
strings: Array of strings to look for.
|
||||
only_new: If true, only check output since last time this method was
|
||||
called. If false, first check earlier output.
|
||||
Returns the matched string.
|
||||
Fails if none of the strings was read after 10 seconds
|
||||
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||
"""
|
||||
if not only_new:
|
||||
full_file = open(filename, "r")
|
||||
for line in full_file:
|
||||
for string in strings:
|
||||
if line.find(string) != -1:
|
||||
full_file.close()
|
||||
return string
|
||||
wait_count = 0
|
||||
while wait_count < OUTPUT_WAIT_MAX_INTERVALS:
|
||||
where = running_file.tell()
|
||||
line = running_file.readline()
|
||||
if line:
|
||||
for string in strings:
|
||||
if line.find(string) != -1:
|
||||
return string
|
||||
else:
|
||||
wait_count += 1
|
||||
time.sleep(OUTPUT_WAIT_INTERVAL)
|
||||
running_file.seek(where)
|
||||
assert False, "Timeout waiting for process output: " + str(strings)
|
||||
|
||||
def wait_for_stderr_str(self, strings, only_new = True):
|
||||
"""
|
||||
Wait for one of the given strings in this process's stderr output.
|
||||
Parameters:
|
||||
strings: Array of strings to look for.
|
||||
only_new: If true, only check output since last time this method was
|
||||
called. If false, first check earlier output.
|
||||
Returns the matched string.
|
||||
Fails if none of the strings was read after 10 seconds
|
||||
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||
"""
|
||||
return self._wait_for_output_str(self.stderr_filename, self.stderr,
|
||||
strings, only_new)
|
||||
|
||||
def wait_for_stdout_str(self, strings, only_new = True):
|
||||
"""
|
||||
Wait for one of the given strings in this process's stdout output.
|
||||
Parameters:
|
||||
strings: Array of strings to look for.
|
||||
only_new: If true, only check output since last time this method was
|
||||
called. If false, first check earlier output.
|
||||
Returns the matched string.
|
||||
Fails if none of the strings was read after 10 seconds
|
||||
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||
"""
|
||||
return self._wait_for_output_str(self.stdout_filename, self.stdout,
|
||||
strings, only_new)
|
||||
|
||||
# Container class for a number of running processes
|
||||
# i.e. servers like bind10, etc
|
||||
# one-shot programs like dig or bindctl are started and closed separately
|
||||
class RunningProcesses:
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize with no running processes.
|
||||
"""
|
||||
self.processes = {}
|
||||
|
||||
def add_process(self, step, process_name, args):
|
||||
"""
|
||||
Start a process with the given arguments, and store it under the given
|
||||
name.
|
||||
Parameters:
|
||||
step: The scenario step it was called from. This is used for
|
||||
determining the output files for redirection of stdout
|
||||
and stderr.
|
||||
process_name: The name to refer to this running process later.
|
||||
args: Array of arguments to pass to Popen().
|
||||
Fails if a process with the given name is already running.
|
||||
"""
|
||||
assert process_name not in self.processes,\
|
||||
"Process " + name + " already running"
|
||||
self.processes[process_name] = RunningProcess(step, process_name, args)
|
||||
|
||||
def get_process(self, process_name):
|
||||
"""
|
||||
Return the Process with the given process name.
|
||||
Parameters:
|
||||
process_name: The name of the process to return.
|
||||
Fails if the process is not running.
|
||||
"""
|
||||
assert process_name in self.processes,\
|
||||
"Process " + name + " unknown"
|
||||
return self.processes[process_name]
|
||||
|
||||
def stop_process(self, process_name):
|
||||
"""
|
||||
Stop the Process with the given process name.
|
||||
Parameters:
|
||||
process_name: The name of the process to return.
|
||||
Fails if the process is not running.
|
||||
"""
|
||||
assert process_name in self.processes,\
|
||||
"Process " + name + " unknown"
|
||||
self.processes[process_name].stop_process()
|
||||
del self.processes[process_name]
|
||||
|
||||
def stop_all_processes(self):
|
||||
"""
|
||||
Stop all running processes.
|
||||
"""
|
||||
for process in self.processes.values():
|
||||
process.stop_process()
|
||||
|
||||
def keep_files(self):
|
||||
"""
|
||||
Keep the redirection files for stdout/stderr output of all processes
|
||||
instead of removing them when they are stopped later.
|
||||
"""
|
||||
for process in self.processes.values():
|
||||
process.remove_files_on_exit = False
|
||||
|
||||
def wait_for_stderr_str(self, process_name, strings, only_new = True):
|
||||
"""
|
||||
Wait for one of the given strings in the given process's stderr output.
|
||||
Parameters:
|
||||
process_name: The name of the process to check the stderr output of.
|
||||
strings: Array of strings to look for.
|
||||
only_new: If true, only check output since last time this method was
|
||||
called. If false, first check earlier output.
|
||||
Returns the matched string.
|
||||
Fails if none of the strings was read after 10 seconds
|
||||
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||
Fails if the process is unknown.
|
||||
"""
|
||||
assert process_name in self.processes,\
|
||||
"Process " + process_name + " unknown"
|
||||
return self.processes[process_name].wait_for_stderr_str(strings,
|
||||
only_new)
|
||||
|
||||
def wait_for_stdout_str(self, process_name, strings, only_new = True):
|
||||
"""
|
||||
Wait for one of the given strings in the given process's stdout output.
|
||||
Parameters:
|
||||
process_name: The name of the process to check the stdout output of.
|
||||
strings: Array of strings to look for.
|
||||
only_new: If true, only check output since last time this method was
|
||||
called. If false, first check earlier output.
|
||||
Returns the matched string.
|
||||
Fails if none of the strings was read after 10 seconds
|
||||
(OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
|
||||
Fails if the process is unknown.
|
||||
"""
|
||||
assert process_name in self.processes,\
|
||||
"Process " + process_name + " unknown"
|
||||
return self.processes[process_name].wait_for_stdout_str(strings,
|
||||
only_new)
|
||||
|
||||
@before.each_scenario
|
||||
def initialize(scenario):
|
||||
"""
|
||||
Global initialization for each scenario.
|
||||
"""
|
||||
# Keep track of running processes
|
||||
world.processes = RunningProcesses()
|
||||
|
||||
# Convenience variable to access the last query result from querying.py
|
||||
world.last_query_result = None
|
||||
|
||||
# Some tests can modify the settings. If the tests fail half-way, or
|
||||
# don't clean up, this can leave configurations or data in a bad state,
|
||||
# so we copy them from originals before each scenario
|
||||
for item in copylist:
|
||||
shutil.copy(item[0], item[1])
|
||||
|
||||
for item in removelist:
|
||||
if os.path.exists(item):
|
||||
os.remove(item)
|
||||
|
||||
@after.each_scenario
|
||||
def cleanup(scenario):
|
||||
"""
|
||||
Global cleanup for each scenario.
|
||||
"""
|
||||
# Keep output files if the scenario failed
|
||||
if not scenario.passed:
|
||||
world.processes.keep_files()
|
||||
# Stop any running processes we may have had around
|
||||
world.processes.stop_all_processes()
|
||||
|
46
tests/lettuce/setup_intree_bind10.sh.in
Executable file
46
tests/lettuce/setup_intree_bind10.sh.in
Executable file
@@ -0,0 +1,46 @@
|
||||
#! /bin/sh
|
||||
|
||||
# Copyright (C) 2010 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.
|
||||
|
||||
PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
|
||||
export PYTHON_EXEC
|
||||
|
||||
BIND10_PATH=@abs_top_builddir@/src/bin/bind10
|
||||
|
||||
PATH=@abs_top_builddir@/src/bin/bind10:@abs_top_builddir@/src/bin/bindctl:@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:$PATH
|
||||
export PATH
|
||||
|
||||
PYTHONPATH=@abs_top_builddir@/src/bin:@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs
|
||||
export PYTHONPATH
|
||||
|
||||
# If necessary (rare cases), explicitly specify paths to dynamic libraries
|
||||
# required by loadable python modules.
|
||||
SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
|
||||
if test $SET_ENV_LIBRARY_PATH = yes; then
|
||||
@ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/acl/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
|
||||
export @ENV_LIBRARY_PATH@
|
||||
fi
|
||||
|
||||
B10_FROM_SOURCE=@abs_top_srcdir@
|
||||
export B10_FROM_SOURCE
|
||||
# TODO: We need to do this feature based (ie. no general from_source)
|
||||
# But right now we need a second one because some spec files are
|
||||
# generated and hence end up under builddir
|
||||
B10_FROM_BUILD=@abs_top_builddir@
|
||||
export B10_FROM_BUILD
|
||||
|
||||
BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
|
||||
export BIND10_MSGQ_SOCKET_FILE
|
Reference in New Issue
Block a user