Browse Source

Merge branch 'trac1290'

Jelte Jansen 13 years ago
parent
commit
6b75c128bc

+ 1 - 0
configure.ac

@@ -991,6 +991,7 @@ AC_OUTPUT([doc/version.ent
            src/lib/util/python/mkpywrapper.py
            src/lib/util/python/mkpywrapper.py
            src/lib/util/python/gen_wiredata.py
            src/lib/util/python/gen_wiredata.py
            src/lib/server_common/tests/data_path.h
            src/lib/server_common/tests/data_path.h
+           tests/lettuce/setup_intree_bind10.sh
            tests/system/conf.sh
            tests/system/conf.sh
            tests/system/run.sh
            tests/system/run.sh
            tests/system/glue/setup.sh
            tests/system/glue/setup.sh

+ 2 - 0
src/bin/bind10/bind10_src.py.in

@@ -675,6 +675,8 @@ class BoB:
         args = ["b10-cmdctl"]
         args = ["b10-cmdctl"]
         if self.cmdctl_port is not None:
         if self.cmdctl_port is not None:
             args.append("--port=" + str(self.cmdctl_port))
             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)
         self.start_process("b10-cmdctl", args, c_channel_env, self.cmdctl_port)
 
 
     def start_all_processes(self):
     def start_all_processes(self):

+ 1 - 2
src/bin/bind10/run_bind10.sh.in

@@ -45,6 +45,5 @@ export B10_FROM_BUILD
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
 export BIND10_MSGQ_SOCKET_FILE
 export BIND10_MSGQ_SOCKET_FILE
 
 
-cd ${BIND10_PATH}
-exec ${PYTHON_EXEC} -O bind10 "$@"
+exec ${PYTHON_EXEC} -O ${BIND10_PATH}/bind10 "$@"
 
 

+ 62 - 57
src/bin/bindctl/bindcmd.py

@@ -71,21 +71,21 @@ Type \"<module_name> <command_name> help\" for help on the specific command.
 \nAvailable module names: """
 \nAvailable module names: """
 
 
 class ValidatedHTTPSConnection(http.client.HTTPSConnection):
 class ValidatedHTTPSConnection(http.client.HTTPSConnection):
-    '''Overrides HTTPSConnection to support certification 
+    '''Overrides HTTPSConnection to support certification
     validation. '''
     validation. '''
     def __init__(self, host, ca_certs):
     def __init__(self, host, ca_certs):
         http.client.HTTPSConnection.__init__(self, host)
         http.client.HTTPSConnection.__init__(self, host)
         self.ca_certs = ca_certs
         self.ca_certs = ca_certs
 
 
     def connect(self):
     def connect(self):
-        ''' Overrides the connect() so that we do 
+        ''' Overrides the connect() so that we do
         certificate validation. '''
         certificate validation. '''
         sock = socket.create_connection((self.host, self.port),
         sock = socket.create_connection((self.host, self.port),
                                         self.timeout)
                                         self.timeout)
         if self._tunnel_host:
         if self._tunnel_host:
             self.sock = sock
             self.sock = sock
             self._tunnel()
             self._tunnel()
-       
+
         req_cert = ssl.CERT_NONE
         req_cert = ssl.CERT_NONE
         if self.ca_certs:
         if self.ca_certs:
             req_cert = ssl.CERT_REQUIRED
             req_cert = ssl.CERT_REQUIRED
@@ -95,7 +95,7 @@ class ValidatedHTTPSConnection(http.client.HTTPSConnection):
                                     ca_certs=self.ca_certs)
                                     ca_certs=self.ca_certs)
 
 
 class BindCmdInterpreter(Cmd):
 class BindCmdInterpreter(Cmd):
-    """simple bindctl example."""    
+    """simple bindctl example."""
 
 
     def __init__(self, server_port='localhost:8080', pem_file=None,
     def __init__(self, server_port='localhost:8080', pem_file=None,
                  csv_file_dir=None):
                  csv_file_dir=None):
@@ -128,29 +128,33 @@ class BindCmdInterpreter(Cmd):
                                       socket.gethostname())).encode())
                                       socket.gethostname())).encode())
         digest = session_id.hexdigest()
         digest = session_id.hexdigest()
         return digest
         return digest
-    
+
     def run(self):
     def run(self):
         '''Parse commands from user and send them to cmdctl. '''
         '''Parse commands from user and send them to cmdctl. '''
         try:
         try:
             if not self.login_to_cmdctl():
             if not self.login_to_cmdctl():
-                return
+                return 1
 
 
             self.cmdloop()
             self.cmdloop()
             print('\nExit from bindctl')
             print('\nExit from bindctl')
+            return 0
         except FailToLogin as err:
         except FailToLogin as err:
             # error already printed when this was raised, ignoring
             # error already printed when this was raised, ignoring
-            pass
+            return 1
         except KeyboardInterrupt:
         except KeyboardInterrupt:
             print('\nExit from bindctl')
             print('\nExit from bindctl')
+            return 0
         except socket.error as err:
         except socket.error as err:
             print('Failed to send request, the connection is closed')
             print('Failed to send request, the connection is closed')
+            return 1
         except http.client.CannotSendRequest:
         except http.client.CannotSendRequest:
             print('Can not send request, the connection is busy')
             print('Can not send request, the connection is busy')
+            return 1
 
 
     def _get_saved_user_info(self, dir, file_name):
     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
         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.'''
         returned.'''
         if (not dir) or (not os.path.exists(dir)):
         if (not dir) or (not os.path.exists(dir)):
             return []
             return []
@@ -176,7 +180,7 @@ class BindCmdInterpreter(Cmd):
             if not os.path.exists(dir):
             if not os.path.exists(dir):
                 os.mkdir(dir, 0o700)
                 os.mkdir(dir, 0o700)
 
 
-            csvfilepath = dir + file_name 
+            csvfilepath = dir + file_name
             csvfile = open(csvfilepath, 'w')
             csvfile = open(csvfilepath, 'w')
             os.chmod(csvfilepath, 0o600)
             os.chmod(csvfilepath, 0o600)
             writer = csv.writer(csvfile)
             writer = csv.writer(csvfile)
@@ -190,7 +194,7 @@ class BindCmdInterpreter(Cmd):
         return True
         return True
 
 
     def login_to_cmdctl(self):
     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
         from user. After the login is sucessful, the username and
         password will be saved in 'default_user.csv', when run the next
         password will be saved in 'default_user.csv', when run the next
         time, username and password saved in 'default_user.csv' will be
         time, username and password saved in 'default_user.csv' will be
@@ -256,14 +260,14 @@ class BindCmdInterpreter(Cmd):
             if self.login_to_cmdctl():
             if self.login_to_cmdctl():
                 # successful, so try send again
                 # successful, so try send again
                 status, reply_msg = self._send_message(url, body)
                 status, reply_msg = self._send_message(url, body)
-            
+
         if reply_msg:
         if reply_msg:
             return json.loads(reply_msg.decode())
             return json.loads(reply_msg.decode())
         else:
         else:
             return {}
             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
         '''Send POST request to cmdctl, session id is send with the name
         'cookie' in header.
         'cookie' in header.
         Format: /module_name/command_name
         Format: /module_name/command_name
@@ -322,12 +326,12 @@ class BindCmdInterpreter(Cmd):
     def _validate_cmd(self, cmd):
     def _validate_cmd(self, cmd):
         '''validate the parameters and merge some parameters together,
         '''validate the parameters and merge some parameters together,
         merge algorithm is based on the command line syntax, later, if
         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:
         if not cmd.module in self.modules:
             raise CmdUnknownModuleSyntaxError(cmd.module)
             raise CmdUnknownModuleSyntaxError(cmd.module)
-        
+
         module_info = self.modules[cmd.module]
         module_info = self.modules[cmd.module]
         if not module_info.has_command_with_name(cmd.command):
         if not module_info.has_command_with_name(cmd.command):
             raise CmdUnknownCmdSyntaxError(cmd.module, 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)
         command_info = module_info.get_command_with_name(cmd.command)
         manda_params = command_info.get_mandatory_param_names()
         manda_params = command_info.get_mandatory_param_names()
         all_params = command_info.get_param_names()
         all_params = command_info.get_param_names()
-        
+
         # If help is entered, don't do further parameter validation.
         # If help is entered, don't do further parameter validation.
         for val in cmd.params.keys():
         for val in cmd.params.keys():
             if val == "help":
             if val == "help":
                 return
                 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:
         elif params and not all_params:
-            raise CmdUnknownParamSyntaxError(cmd.module, cmd.command, 
+            raise CmdUnknownParamSyntaxError(cmd.module, cmd.command,
                                              list(params.keys())[0])
                                              list(params.keys())[0])
         elif params:
         elif params:
             param_name = None
             param_name = None
@@ -376,7 +380,7 @@ class BindCmdInterpreter(Cmd):
                         param_name = command_info.get_param_name_by_position(name, param_count)
                         param_name = command_info.get_param_name_by_position(name, param_count)
                         cmd.params[param_name] = cmd.params[name]
                         cmd.params[param_name] = cmd.params[name]
                         del cmd.params[name]
                         del cmd.params[name]
-                        
+
                 elif not name in all_params:
                 elif not name in all_params:
                     raise CmdUnknownParamSyntaxError(cmd.module, cmd.command, name)
                     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:
                 if not name in params and not param_nr in params:
                     raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
                     raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
                 param_nr += 1
                 param_nr += 1
-        
+
         # Convert parameter value according parameter spec file.
         # Convert parameter value according parameter spec file.
         # Ignore check for commands belongs to module 'config'
         # Ignore check for commands belongs to module 'config'
         if cmd.module != CONFIG_MODULE_NAME:
         if cmd.module != CONFIG_MODULE_NAME:
@@ -394,9 +398,9 @@ class BindCmdInterpreter(Cmd):
                 try:
                 try:
                     cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
                     cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
                 except isc.cc.data.DataTypeError as e:
                 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))
                                                      % (param_name, param_spec['item_type']) + str(e))
-    
+
     def _handle_cmd(self, cmd):
     def _handle_cmd(self, cmd):
         '''Handle a command entered by the user'''
         '''Handle a command entered by the user'''
         if cmd.command == "help" or ("help" in cmd.params.keys()):
         if cmd.command == "help" or ("help" in cmd.params.keys()):
@@ -418,7 +422,7 @@ class BindCmdInterpreter(Cmd):
     def add_module_info(self, module_info):
     def add_module_info(self, module_info):
         '''Add the information about one module'''
         '''Add the information about one module'''
         self.modules[module_info.name] = module_info
         self.modules[module_info.name] = module_info
-        
+
     def get_module_names(self):
     def get_module_names(self):
         '''Return the names of all known modules'''
         '''Return the names of all known modules'''
         return list(self.modules.keys())
         return list(self.modules.keys())
@@ -450,15 +454,15 @@ class BindCmdInterpreter(Cmd):
                     subsequent_indent="    " +
                     subsequent_indent="    " +
                     " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
                     " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
                     width=70))
                     width=70))
-    
+
     def onecmd(self, line):
     def onecmd(self, line):
         if line == 'EOF' or line.lower() == "quit":
         if line == 'EOF' or line.lower() == "quit":
             self.conn.close()
             self.conn.close()
             return True
             return True
-            
+
         if line == 'h':
         if line == 'h':
             line = 'help'
             line = 'help'
-        
+
         Cmd.onecmd(self, line)
         Cmd.onecmd(self, line)
 
 
     def remove_prefix(self, list, prefix):
     def remove_prefix(self, list, prefix):
@@ -486,7 +490,7 @@ class BindCmdInterpreter(Cmd):
                 cmd = BindCmdParse(cur_line)
                 cmd = BindCmdParse(cur_line)
                 if not cmd.params and text:
                 if not cmd.params and text:
                     hints = self._get_command_startswith(cmd.module, text)
                     hints = self._get_command_startswith(cmd.module, text)
-                else:                       
+                else:
                     hints = self._get_param_startswith(cmd.module, cmd.command,
                     hints = self._get_param_startswith(cmd.module, cmd.command,
                                                        text)
                                                        text)
                     if cmd.module == CONFIG_MODULE_NAME:
                     if cmd.module == CONFIG_MODULE_NAME:
@@ -502,8 +506,8 @@ class BindCmdInterpreter(Cmd):
 
 
             except CmdMissCommandNameFormatError as e:
             except CmdMissCommandNameFormatError as e:
                 if not text.strip(): # command name is empty
                 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)
                     hints = self._get_module_startswith(text)
 
 
             except CmdCommandNameFormatError as e:
             except CmdCommandNameFormatError as e:
@@ -523,36 +527,37 @@ class BindCmdInterpreter(Cmd):
         else:
         else:
             return None
             return None
 
 
-    def _get_module_startswith(self, text):       
+
+    def _get_module_startswith(self, text):
         return [module
         return [module
-                for module in self.modules 
+                for module in self.modules
                 if module.startswith(text)]
                 if module.startswith(text)]
 
 
 
 
     def _get_command_startswith(self, module, text):
     def _get_command_startswith(self, module, text):
-        if module in self.modules:            
+        if module in self.modules:
             return [command
             return [command
-                    for command in self.modules[module].get_command_names() 
+                    for command in self.modules[module].get_command_names()
                     if command.startswith(text)]
                     if command.startswith(text)]
-        
-        return []                    
-                        
 
 
-    def _get_param_startswith(self, module, command, text):        
+        return []
+
+
+    def _get_param_startswith(self, module, command, text):
         if module in self.modules:
         if module in self.modules:
-            module_info = self.modules[module]            
-            if command in module_info.get_command_names():                
+            module_info = self.modules[module]
+            if command in module_info.get_command_names():
                 cmd_info = module_info.get_command_with_name(command)
                 cmd_info = module_info.get_command_with_name(command)
-                params = cmd_info.get_param_names() 
+                params = cmd_info.get_param_names()
                 hint = []
                 hint = []
-                if text:    
+                if text:
                     hint = [val for val in params if val.startswith(text)]
                     hint = [val for val in params if val.startswith(text)]
                 else:
                 else:
                     hint = list(params)
                     hint = list(params)
-                
+
                 if len(hint) == 1 and hint[0] != "help":
                 if len(hint) == 1 and hint[0] != "help":
-                    hint[0] = hint[0] + " ="    
-                
+                    hint[0] = hint[0] + " ="
+
                 return hint
                 return hint
 
 
         return []
         return []
@@ -569,24 +574,24 @@ class BindCmdInterpreter(Cmd):
             self._print_correct_usage(err)
             self._print_correct_usage(err)
         except isc.cc.data.DataTypeError as err:
         except isc.cc.data.DataTypeError as err:
             print("Error! ", err)
             print("Error! ", err)
-            
-    def _print_correct_usage(self, ept):        
+
+    def _print_correct_usage(self, ept):
         if isinstance(ept, CmdUnknownModuleSyntaxError):
         if isinstance(ept, CmdUnknownModuleSyntaxError):
             self.do_help(None)
             self.do_help(None)
-            
+
         elif isinstance(ept, CmdUnknownCmdSyntaxError):
         elif isinstance(ept, CmdUnknownCmdSyntaxError):
             self.modules[ept.module].module_help()
             self.modules[ept.module].module_help()
-            
+
         elif isinstance(ept, CmdMissParamSyntaxError) or \
         elif isinstance(ept, CmdMissParamSyntaxError) or \
              isinstance(ept, CmdUnknownParamSyntaxError):
              isinstance(ept, CmdUnknownParamSyntaxError):
              self.modules[ept.module].command_help(ept.command)
              self.modules[ept.module].command_help(ept.command)
-                 
-                
+
+
     def _append_space_to_hint(self):
     def _append_space_to_hint(self):
         """Append one space at the end of complete hint."""
         """Append one space at the end of complete hint."""
         self.hint = [(val + " ") for val in self.hint]
         self.hint = [(val + " ") for val in self.hint]
-            
-            
+
+
     def _handle_help(self, cmd):
     def _handle_help(self, cmd):
         if cmd.command == "help":
         if cmd.command == "help":
             self.modules[cmd.module].module_help()
             self.modules[cmd.module].module_help()

+ 2 - 1
src/bin/bindctl/bindctl_main.py.in

@@ -146,4 +146,5 @@ if __name__ == '__main__':
     tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
     tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
                               csv_file_dir=options.csv_file_dir)
                               csv_file_dir=options.csv_file_dir)
     prepare_config_commands(tool)
     prepare_config_commands(tool)
-    tool.run()
+    result = tool.run()
+    sys.exit(result)

+ 63 - 63
src/bin/bindctl/tests/bindctl_test.py

@@ -31,14 +31,14 @@ from bindctl_main import set_bindctl_options
 from bindctl import cmdparse
 from bindctl import cmdparse
 from bindctl import bindcmd
 from bindctl import bindcmd
 from bindctl.moduleinfo import *
 from bindctl.moduleinfo import *
-from bindctl.exception import *    
+from bindctl.exception import *
 try:
 try:
     from collections import OrderedDict
     from collections import OrderedDict
 except ImportError:
 except ImportError:
     from mycollections import OrderedDict
     from mycollections import OrderedDict
 
 
 class TestCmdLex(unittest.TestCase):
 class TestCmdLex(unittest.TestCase):
-    
+
     def my_assert_raise(self, exception_type, cmd_line):
     def my_assert_raise(self, exception_type, cmd_line):
         self.assertRaises(exception_type, cmdparse.BindCmdParse, cmd_line)
         self.assertRaises(exception_type, cmdparse.BindCmdParse, cmd_line)
 
 
@@ -48,13 +48,13 @@ class TestCmdLex(unittest.TestCase):
         assert cmd.module == "zone"
         assert cmd.module == "zone"
         assert cmd.command == "add"
         assert cmd.command == "add"
         self.assertEqual(len(cmd.params), 0)
         self.assertEqual(len(cmd.params), 0)
-        
-    
+
+
     def testCommandWithParameters(self):
     def testCommandWithParameters(self):
         lines = {"zone add zone_name = cnnic.cn, file = cnnic.cn.file master=1.1.1.1",
         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  ",
                  "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:
         for cmd_line in lines:
             cmd = cmdparse.BindCmdParse(cmd_line)
             cmd = cmdparse.BindCmdParse(cmd_line)
             assert cmd.module == "zone"
             assert cmd.module == "zone"
@@ -75,7 +75,7 @@ class TestCmdLex(unittest.TestCase):
         cmd = cmdparse.BindCmdParse('zone cmd name = 1\"\'34**&2 ,value=  44\"\'\"')
         cmd = cmdparse.BindCmdParse('zone cmd name = 1\"\'34**&2 ,value=  44\"\'\"')
         self.assertEqual(cmd.params['name'], '1\"\'34**&2')
         self.assertEqual(cmd.params['name'], '1\"\'34**&2')
         self.assertEqual(cmd.params['value'], '44\"\'\"')
         self.assertEqual(cmd.params['value'], '44\"\'\"')
-            
+
         cmd = cmdparse.BindCmdParse('zone cmd name =  1\'34**&2value=44\"\'\" value = \"==============\'')
         cmd = cmdparse.BindCmdParse('zone cmd name =  1\'34**&2value=44\"\'\" value = \"==============\'')
         self.assertEqual(cmd.params['name'], '1\'34**&2value=44\"\'\"')
         self.assertEqual(cmd.params['name'], '1\'34**&2value=44\"\'\"')
         self.assertEqual(cmd.params['value'], '==============')
         self.assertEqual(cmd.params['value'], '==============')
@@ -83,34 +83,34 @@ class TestCmdLex(unittest.TestCase):
         cmd = cmdparse.BindCmdParse('zone cmd name =    \"1234, 567890 \" value ==&*/')
         cmd = cmdparse.BindCmdParse('zone cmd name =    \"1234, 567890 \" value ==&*/')
         self.assertEqual(cmd.params['name'], '1234, 567890 ')
         self.assertEqual(cmd.params['name'], '1234, 567890 ')
         self.assertEqual(cmd.params['value'], '=&*/')
         self.assertEqual(cmd.params['value'], '=&*/')
-            
+
     def testCommandWithListParam(self):
     def testCommandWithListParam(self):
         cmd = cmdparse.BindCmdParse("zone set zone_name='cnnic.cn', master='1.1.1.1, 2.2.2.2'")
         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):
     def testCommandWithHelpParam(self):
         cmd = cmdparse.BindCmdParse("zone add help")
         cmd = cmdparse.BindCmdParse("zone add help")
         assert cmd.params["help"] == "help"
         assert cmd.params["help"] == "help"
-        
+
         cmd = cmdparse.BindCmdParse("zone add help *&)&)*&&$#$^%")
         cmd = cmdparse.BindCmdParse("zone add help *&)&)*&&$#$^%")
         assert cmd.params["help"] == "help"
         assert cmd.params["help"] == "help"
         self.assertEqual(len(cmd.params), 1)
         self.assertEqual(len(cmd.params), 1)
-        
+
 
 
     def testCmdModuleNameFormatError(self):
     def testCmdModuleNameFormatError(self):
         self.my_assert_raise(CmdModuleNameFormatError, "zone=good")
         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,")        
-        
-        
+        self.my_assert_raise(CmdModuleNameFormatError, "zone,")
+
+
     def testCmdMissCommandNameFormatError(self):
     def testCmdMissCommandNameFormatError(self):
         self.my_assert_raise(CmdMissCommandNameFormatError, "zone")
         self.my_assert_raise(CmdMissCommandNameFormatError, "zone")
         self.my_assert_raise(CmdMissCommandNameFormatError, "zone ")
         self.my_assert_raise(CmdMissCommandNameFormatError, "zone ")
         self.my_assert_raise(CmdMissCommandNameFormatError, "help ")
         self.my_assert_raise(CmdMissCommandNameFormatError, "help ")
-        
-             
+
+
     def testCmdCommandNameFormatError(self):
     def testCmdCommandNameFormatError(self):
         self.my_assert_raise(CmdCommandNameFormatError, "zone =d")
         self.my_assert_raise(CmdCommandNameFormatError, "zone =d")
         self.my_assert_raise(CmdCommandNameFormatError, "zone z=d")
         self.my_assert_raise(CmdCommandNameFormatError, "zone z=d")
@@ -119,11 +119,11 @@ class TestCmdLex(unittest.TestCase):
         self.my_assert_raise(CmdCommandNameFormatError, "zone zdd/ \"")
         self.my_assert_raise(CmdCommandNameFormatError, "zone zdd/ \"")
 
 
 class TestCmdSyntax(unittest.TestCase):
 class TestCmdSyntax(unittest.TestCase):
-    
+
     def _create_bindcmd(self):
     def _create_bindcmd(self):
         """Create one bindcmd"""
         """Create one bindcmd"""
-        
-        tool = bindcmd.BindCmdInterpreter()        
+
+        tool = bindcmd.BindCmdInterpreter()
         string_spec = { 'item_type' : 'string',
         string_spec = { 'item_type' : 'string',
                        'item_optional' : False,
                        'item_optional' : False,
                        'item_default' : ''}
                        'item_default' : ''}
@@ -135,40 +135,40 @@ class TestCmdSyntax(unittest.TestCase):
         load_cmd = CommandInfo(name = "load")
         load_cmd = CommandInfo(name = "load")
         load_cmd.add_param(zone_file_param)
         load_cmd.add_param(zone_file_param)
         load_cmd.add_param(zone_name)
         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 = CommandInfo(name = "set")
         set_cmd.add_param(param_master)
         set_cmd.add_param(param_master)
         set_cmd.add_param(param_allow_update)
         set_cmd.add_param(param_allow_update)
         set_cmd.add_param(zone_name)
         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(load_cmd)
         zone_module.add_command(set_cmd)
         zone_module.add_command(set_cmd)
         zone_module.add_command(reload_all_cmd)
         zone_module.add_command(reload_all_cmd)
-        
+
         tool.add_module_info(zone_module)
         tool.add_module_info(zone_module)
         return tool
         return tool
-        
-        
+
+
     def setUp(self):
     def setUp(self):
         self.bindcmd = self._create_bindcmd()
         self.bindcmd = self._create_bindcmd()
-        
-        
+
+
     def no_assert_raise(self, cmd_line):
     def no_assert_raise(self, cmd_line):
         cmd = cmdparse.BindCmdParse(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):
     def my_assert_raise(self, exception_type, cmd_line):
         cmd = cmdparse.BindCmdParse(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):
     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'")
         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 allow_update='1.1.1.1' zone_name='cn'")
         self.no_assert_raise("zone set 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.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):
     def testCmdUnknownModuleSyntaxError(self):
         self.my_assert_raise(CmdUnknownModuleSyntaxError, "zoned d")
         self.my_assert_raise(CmdUnknownModuleSyntaxError, "zoned d")
         self.my_assert_raise(CmdUnknownModuleSyntaxError, "dd dd  ")
         self.my_assert_raise(CmdUnknownModuleSyntaxError, "dd dd  ")
-        
-              
+
+
     def testCmdUnknownCmdSyntaxError(self):
     def testCmdUnknownCmdSyntaxError(self):
         self.my_assert_raise(CmdUnknownCmdSyntaxError, "zone dd")
         self.my_assert_raise(CmdUnknownCmdSyntaxError, "zone dd")
-        
+
     def testCmdMissParamSyntaxError(self):
     def testCmdMissParamSyntaxError(self):
         self.my_assert_raise(CmdMissParamSyntaxError, "zone load zone_file='cn'")
         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 load zone_name='cn'")
         self.my_assert_raise(CmdMissParamSyntaxError, "zone set allow_update='1.1.1.1'")
         self.my_assert_raise(CmdMissParamSyntaxError, "zone set allow_update='1.1.1.1'")
         self.my_assert_raise(CmdMissParamSyntaxError, "zone set ")
         self.my_assert_raise(CmdMissParamSyntaxError, "zone set ")
-        
+
     def testCmdUnknownParamSyntaxError(self):
     def testCmdUnknownParamSyntaxError(self):
         self.my_assert_raise(CmdUnknownParamSyntaxError, "zone load zone_d='cn'")
         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):
 class TestModuleInfo(unittest.TestCase):
 
 
     def test_get_param_name_by_position(self):
     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('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.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)
         self.assertRaises(KeyError, cmd.get_param_name_by_position, 4, 4)
 
 
 
 
-    
+
 class TestNameSequence(unittest.TestCase):
 class TestNameSequence(unittest.TestCase):
     """
     """
     Test if the module/command/parameters is saved in the order creation
     Test if the module/command/parameters is saved in the order creation
     """
     """
-    
+
     def _create_bindcmd(self):
     def _create_bindcmd(self):
-        """Create one bindcmd"""     
-        
+        """Create one bindcmd"""
+
         self._cmd = CommandInfo(name = "load")
         self._cmd = CommandInfo(name = "load")
         self.module = ModuleInfo(name = "zone")
         self.module = ModuleInfo(name = "zone")
-        self.tool = bindcmd.BindCmdInterpreter()        
+        self.tool = bindcmd.BindCmdInterpreter()
         for random_str in self.random_names:
         for random_str in self.random_names:
             self._cmd.add_param(ParamInfo(name = random_str))
             self._cmd.add_param(ParamInfo(name = random_str))
             self.module.add_command(CommandInfo(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):
     def setUp(self):
         self.random_names = ['1erdfeDDWsd', '3fe', '2009erd', 'Fe231', 'tere142', 'rei8WD']
         self.random_names = ['1erdfeDDWsd', '3fe', '2009erd', 'Fe231', 'tere142', 'rei8WD']
         self._create_bindcmd()
         self._create_bindcmd()
-        
-    def testSequence(self):        
+
+    def testSequence(self):
         param_names = self._cmd.get_param_names()
         param_names = self._cmd.get_param_names()
         cmd_names = self.module.get_command_names()
         cmd_names = self.module.get_command_names()
         module_names = self.tool.get_module_names()
         module_names = self.tool.get_module_names()
-        
+
         i = 0
         i = 0
         while i < len(self.random_names):
         while i < len(self.random_names):
             assert self.random_names[i] == param_names[i+1]
             assert self.random_names[i] == param_names[i+1]
@@ -342,7 +342,7 @@ class TestConfigCommands(unittest.TestCase):
         # validate log message for socket.err
         # validate log message for socket.err
         socket_err_output = io.StringIO()
         socket_err_output = io.StringIO()
         sys.stdout = socket_err_output
         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",
         self.assertEqual("Failed to send request, the connection is closed\n",
                          socket_err_output.getvalue())
                          socket_err_output.getvalue())
         socket_err_output.close()
         socket_err_output.close()
@@ -350,7 +350,7 @@ class TestConfigCommands(unittest.TestCase):
         # validate log message for http.client.CannotSendRequest
         # validate log message for http.client.CannotSendRequest
         cannot_send_output = io.StringIO()
         cannot_send_output = io.StringIO()
         sys.stdout = cannot_send_output
         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",
         self.assertEqual("Can not send request, the connection is busy\n",
                          cannot_send_output.getvalue())
                          cannot_send_output.getvalue())
         cannot_send_output.close()
         cannot_send_output.close()
@@ -472,4 +472,4 @@ class TestCommandLineOptions(unittest.TestCase):
 
 
 if __name__== "__main__":
 if __name__== "__main__":
     unittest.main()
     unittest.main()
-    
+

+ 48 - 45
src/bin/cmdctl/cmdctl.py.in

@@ -17,12 +17,12 @@
 
 
 ''' cmdctl module is the configuration entry point for all commands from bindctl
 ''' 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-
 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
 configuration specification/data of other available modules from configmanager, then
 wait for receiving request from client, parse the request and resend the request to
 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.
 resut to client.
 '''
 '''
 
 
@@ -81,16 +81,16 @@ SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
 
 
 class CmdctlException(Exception):
 class CmdctlException(Exception):
     pass
     pass
-       
+
 class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
 class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
     '''https connection request handler.
     '''https connection request handler.
     Currently only GET and POST are supported.  '''
     Currently only GET and POST are supported.  '''
     def do_GET(self):
     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'
         the name 'cookie'
         '''
         '''
         self.session_id = self.headers.get('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_session_valid():
             if self._is_user_logged_in():
             if self._is_user_logged_in():
                 rcode, reply = self._handle_get_request()
                 rcode, reply = self._handle_get_request()
@@ -106,16 +106,16 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
     def _handle_get_request(self):
     def _handle_get_request(self):
         '''Currently only support the following three url GET request '''
         '''Currently only support the following three url GET request '''
         id, module = self._parse_request_path()
         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):
     def _is_session_valid(self):
-        return self.session_id 
+        return self.session_id
 
 
     def _is_user_logged_in(self):
     def _is_user_logged_in(self):
         login_time = self.server.user_sessions.get(self.session_id)
         login_time = self.server.user_sessions.get(self.session_id)
         if not login_time:
         if not login_time:
             return False
             return False
-        
+
         idle_time = time.time() - login_time
         idle_time = time.time() - login_time
         if idle_time > self.server.idle_timeout:
         if idle_time > self.server.idle_timeout:
             return False
             return False
@@ -125,7 +125,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
 
 
     def _parse_request_path(self):
     def _parse_request_path(self):
         '''Parse the url, the legal url should like /ldh or /ldh/ldh '''
         '''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:
         if not groups:
             return (None, None)
             return (None, None)
         else:
         else:
@@ -133,8 +133,8 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
 
 
     def do_POST(self):
     def do_POST(self):
         '''Process POST request. '''
         '''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'
         the name 'cookie'
         '''
         '''
         self.session_id = self.headers.get('cookie')
         self.session_id = self.headers.get('cookie')
@@ -148,7 +148,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
                 rcode, reply = http.client.UNAUTHORIZED, ["please login"]
                 rcode, reply = http.client.UNAUTHORIZED, ["please login"]
         else:
         else:
             rcode, reply = http.client.BAD_REQUEST, ["session isn't valid"]
             rcode, reply = http.client.BAD_REQUEST, ["session isn't valid"]
-      
+
         self.send_response(rcode)
         self.send_response(rcode)
         self.end_headers()
         self.end_headers()
         self.wfile.write(json.dumps(reply).encode())
         self.wfile.write(json.dumps(reply).encode())
@@ -169,12 +169,12 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
         length = self.headers.get('Content-Length')
         length = self.headers.get('Content-Length')
 
 
         if not length:
         if not length:
-            return False, ["invalid username or password"]     
+            return False, ["invalid username or password"]
 
 
         try:
         try:
             user_info = json.loads((self.rfile.read(int(length))).decode())
             user_info = json.loads((self.rfile.read(int(length))).decode())
         except:
         except:
-            return False, ["invalid username or password"]                
+            return False, ["invalid username or password"]
 
 
         user_name = user_info.get('username')
         user_name = user_info.get('username')
         if not user_name:
         if not user_name:
@@ -193,7 +193,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
             return False, ["username or password error"]
             return False, ["username or password error"]
 
 
         return True, None
         return True, None
-   
+
 
 
     def _handle_post_request(self):
     def _handle_post_request(self):
         '''Handle all the post request from client. '''
         '''Handle all the post request from client. '''
@@ -215,7 +215,7 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
         if rcode != 0:
         if rcode != 0:
             ret = http.client.BAD_REQUEST
             ret = http.client.BAD_REQUEST
         return ret, reply
         return ret, reply
-    
+
     def log_request(self, code='-', size='-'):
     def log_request(self, code='-', size='-'):
         '''Rewrite the log request function, log nothing.'''
         '''Rewrite the log request function, log nothing.'''
         pass
         pass
@@ -239,11 +239,11 @@ class CommandControl():
 
 
     def _setup_session(self):
     def _setup_session(self):
         '''Setup the session for receving the commands
         '''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._cc = isc.cc.Session()
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
         self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                               self.config_handler,
                                               self.config_handler,
@@ -251,7 +251,7 @@ class CommandControl():
         self._module_name = self._module_cc.get_module_spec().get_module_name()
         self._module_name = self._module_cc.get_module_spec().get_module_name()
         self._cmdctl_config_data = self._module_cc.get_full_config()
         self._cmdctl_config_data = self._module_cc.get_full_config()
         self._module_cc.start()
         self._module_cc.start()
-    
+
     def _accounts_file_check(self, filepath):
     def _accounts_file_check(self, filepath):
         ''' Check whether the accounts file is valid, each row
         ''' Check whether the accounts file is valid, each row
         should be a list with 3 items.'''
         should be a list with 3 items.'''
@@ -288,7 +288,7 @@ class CommandControl():
                 errstr = self._accounts_file_check(new_config[key])
                 errstr = self._accounts_file_check(new_config[key])
             else:
             else:
                 errstr = 'unknown config item: ' + key
                 errstr = 'unknown config item: ' + key
-            
+
             if errstr != None:
             if errstr != None:
                 logger.error(CMDCTL_BAD_CONFIG_DATA, errstr);
                 logger.error(CMDCTL_BAD_CONFIG_DATA, errstr);
                 return ccsession.create_answer(1, errstr)
                 return ccsession.create_answer(1, errstr)
@@ -314,7 +314,7 @@ class CommandControl():
                 self.modules_spec[args[0]] = args[1]
                 self.modules_spec[args[0]] = args[1]
 
 
         elif command == ccsession.COMMAND_SHUTDOWN:
         elif command == ccsession.COMMAND_SHUTDOWN:
-            #When cmdctl get 'shutdown' command from boss, 
+            #When cmdctl get 'shutdown' command from boss,
             #shutdown the outer httpserver.
             #shutdown the outer httpserver.
             self._httpserver.shutdown()
             self._httpserver.shutdown()
             self._serving = False
             self._serving = False
@@ -384,12 +384,12 @@ class CommandControl():
         specs = self.get_modules_spec()
         specs = self.get_modules_spec()
         if module_name not in specs.keys():
         if module_name not in specs.keys():
             return 1, {'error' : 'unknown module'}
             return 1, {'error' : 'unknown module'}
-       
+
         spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
         spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
         errors = []
         errors = []
         if not spec_obj.validate_command(command_name, params, errors):
         if not spec_obj.validate_command(command_name, params, errors):
             return 1, {'error': errors[0]}
             return 1, {'error': errors[0]}
-        
+
         return self.send_command(module_name, command_name, params)
         return self.send_command(module_name, command_name, params)
 
 
     def send_command(self, module_name, command_name, params = None):
     def send_command(self, module_name, command_name, params = None):
@@ -400,7 +400,7 @@ class CommandControl():
                      command_name, module_name)
                      command_name, module_name)
 
 
         if module_name == self._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)
             answer = self.command_handler(command_name, params)
         else:
         else:
             msg = ccsession.create_command(command_name, params)
             msg = ccsession.create_command(command_name, params)
@@ -429,7 +429,7 @@ class CommandControl():
 
 
         logger.error(CMDCTL_COMMAND_ERROR, command_name, module_name, errstr)
         logger.error(CMDCTL_COMMAND_ERROR, command_name, module_name, errstr)
         return 1, {'error': errstr}
         return 1, {'error': errstr}
-    
+
     def get_cmdctl_config_data(self):
     def get_cmdctl_config_data(self):
         ''' If running in source code tree, use keyfile, certificate
         ''' If running in source code tree, use keyfile, certificate
         and user accounts file in source code. '''
         and user accounts file in source code. '''
@@ -453,13 +453,15 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
     '''Make the server address can be reused.'''
     '''Make the server address can be reused.'''
     allow_reuse_address = True
     allow_reuse_address = True
 
 
-    def __init__(self, server_address, RequestHandlerClass, 
+    def __init__(self, server_address, RequestHandlerClass,
                  CommandControlClass,
                  CommandControlClass,
                  idle_timeout = 1200, verbose = False):
                  idle_timeout = 1200, verbose = False):
         '''idle_timeout: the max idle time for login'''
         '''idle_timeout: the max idle time for login'''
         socketserver_mixin.NoPollMixIn.__init__(self)
         socketserver_mixin.NoPollMixIn.__init__(self)
         try:
         try:
             http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
             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:
         except socket.error as err:
             raise CmdctlException("Error creating server, because: %s \n" % str(err))
             raise CmdctlException("Error creating server, because: %s \n" % str(err))
 
 
@@ -472,9 +474,9 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
         self._accounts_file = None
         self._accounts_file = None
 
 
     def _create_user_info(self, accounts_file):
     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.'''
         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
             return
 
 
         with self._lock:
         with self._lock:
@@ -495,10 +497,10 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
         self._accounts_file = accounts_file
         self._accounts_file = accounts_file
         if len(self._user_infos) == 0:
         if len(self._user_infos) == 0:
             logger.error(CMDCTL_NO_USER_ENTRIES_READ)
             logger.error(CMDCTL_NO_USER_ENTRIES_READ)
-         
+
     def get_user_info(self, username):
     def get_user_info(self, username):
         '''Get user's salt and hashed string. If the user
         '''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.'''
         [salt, hashed password] will be returned.'''
         with self._lock:
         with self._lock:
             info = self._user_infos.get(username)
             info = self._user_infos.get(username)
@@ -507,9 +509,9 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
     def save_user_session_id(self, session_id):
     def save_user_session_id(self, session_id):
         ''' Record user's id and login time. '''
         ''' Record user's id and login time. '''
         self.user_sessions[session_id] = time.time()
         self.user_sessions[session_id] = time.time()
-        
+
     def _check_key_and_cert(self, key, cert):
     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):
         if not os.path.exists(key):
             raise CmdctlException("key file '%s' doesn't exist " % key)
             raise CmdctlException("key file '%s' doesn't exist " % key)
 
 
@@ -524,7 +526,7 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
                                       certfile = cert,
                                       certfile = cert,
                                       keyfile = key,
                                       keyfile = key,
                                       ssl_version = ssl.PROTOCOL_SSLv23)
                                       ssl_version = ssl.PROTOCOL_SSLv23)
-            return ssl_sock 
+            return ssl_sock
         except (ssl.SSLError, CmdctlException) as err :
         except (ssl.SSLError, CmdctlException) as err :
             logger.info(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
             logger.info(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
             self.close_request(sock)
             self.close_request(sock)
@@ -541,18 +543,18 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
 
 
     def get_reply_data_for_GET(self, id, module):
     def get_reply_data_for_GET(self, id, module):
         '''Currently only support the following three url GET request '''
         '''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 not module:
             if id == CONFIG_DATA_URL:
             if id == CONFIG_DATA_URL:
                 rcode, reply = http.client.OK, self.cmdctl.get_config_data()
                 rcode, reply = http.client.OK, self.cmdctl.get_config_data()
             elif id == MODULE_SPEC_URL:
             elif id == MODULE_SPEC_URL:
                 rcode, reply = http.client.OK, self.cmdctl.get_modules_spec()
                 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):
     def send_command_to_module(self, module_name, command_name, params):
         return self.cmdctl.send_command_with_check(module_name, command_name, params)
         return self.cmdctl.send_command_with_check(module_name, command_name, params)
-   
+
 httpd = None
 httpd = None
 
 
 def signal_handler(signal, frame):
 def signal_handler(signal, frame):
@@ -566,10 +568,9 @@ def set_signal_handler():
 
 
 def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
 def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
     ''' Start cmdctl as one https server. '''
     ''' 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)
                              CommandControl, idle_timeout, verbose)
+
     httpd.serve_forever()
     httpd.serve_forever()
 
 
 def check_port(option, opt_str, value, parser):
 def check_port(option, opt_str, value, parser):
@@ -607,6 +608,8 @@ if __name__ == '__main__':
     (options, args) = parser.parse_args()
     (options, args) = parser.parse_args()
     result = 1                  # in case of failure
     result = 1                  # in case of failure
     try:
     try:
+        if options.verbose:
+            logger.set_severity("DEBUG", 99)
         run(options.addr, options.port, options.idle_timeout, options.verbose)
         run(options.addr, options.port, options.idle_timeout, options.verbose)
         result = 0
         result = 0
     except isc.cc.SessionError as err:
     except isc.cc.SessionError as err:

+ 3 - 0
src/bin/cmdctl/cmdctl_messages.mes

@@ -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
 causes may be that the ssl request itself was bad, or the local key or
 certificate file could not be read.
 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
 % CMDCTL_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
 There was a keyboard interrupt signal to stop the cmdctl daemon. The
 There was a keyboard interrupt signal to stop the cmdctl daemon. The
 daemon will now shut down.
 daemon will now shut down.

+ 1 - 0
src/lib/python/isc/bind10/sockcreator.py

@@ -18,6 +18,7 @@ import struct
 import os
 import os
 import copy
 import copy
 import subprocess
 import subprocess
+import copy
 from isc.log_messages.bind10_messages import *
 from isc.log_messages.bind10_messages import *
 from libutil_io_python import recv_fd
 from libutil_io_python import recv_fd
 
 

+ 6 - 5
src/lib/python/isc/config/cfgmgr.py

@@ -117,12 +117,13 @@ class ConfigManagerData:
             if file:
             if file:
                 file.close();
                 file.close();
         return config
         return config
-        
+
     def write_to_file(self, output_file_name = None):
     def write_to_file(self, output_file_name = None):
         """Writes the current configuration data to a file. If
         """Writes the current configuration data to a file. If
            output_file_name is not specified, the file used in
            output_file_name is not specified, the file used in
            read_from_file is used."""
            read_from_file is used."""
         filename = None
         filename = None
+
         try:
         try:
             file = tempfile.NamedTemporaryFile(mode='w',
             file = tempfile.NamedTemporaryFile(mode='w',
                                                prefix="b10-config.db.",
                                                prefix="b10-config.db.",
@@ -291,7 +292,7 @@ class ConfigManager:
             # ok, just start with an empty config
             # ok, just start with an empty config
             self.config = ConfigManagerData(self.data_path,
             self.config = ConfigManagerData(self.data_path,
                                             self.database_filename)
                                             self.database_filename)
-        
+
     def write_config(self):
     def write_config(self):
         """Write the current configuration to the file specificied at init()"""
         """Write the current configuration to the file specificied at init()"""
         self.config.write_to_file()
         self.config.write_to_file()
@@ -445,7 +446,7 @@ class ConfigManager:
             answer = ccsession.create_answer(1, "Wrong number of arguments")
             answer = ccsession.create_answer(1, "Wrong number of arguments")
         if not answer:
         if not answer:
             answer = ccsession.create_answer(1, "No answer message from " + cmd[0])
             answer = ccsession.create_answer(1, "No answer message from " + cmd[0])
-            
+
         return answer
         return answer
 
 
     def _handle_module_spec(self, spec):
     def _handle_module_spec(self, spec):
@@ -455,7 +456,7 @@ class ConfigManager:
         # todo: error checking (like keyerrors)
         # todo: error checking (like keyerrors)
         answer = {}
         answer = {}
         self.set_module_spec(spec)
         self.set_module_spec(spec)
-        
+
         # We should make one general 'spec update for module' that
         # We should make one general 'spec update for module' that
         # passes both specification and commands at once
         # passes both specification and commands at once
         spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
         spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
@@ -491,7 +492,7 @@ class ConfigManager:
         else:
         else:
             answer = ccsession.create_answer(1, "Unknown message format: " + str(msg))
             answer = ccsession.create_answer(1, "Unknown message format: " + str(msg))
         return answer
         return answer
-        
+
     def run(self):
     def run(self):
         """Runs the configuration manager."""
         """Runs the configuration manager."""
         self.running = True
         self.running = True

+ 7 - 7
src/lib/python/isc/config/tests/cfgmgr_test.py

@@ -37,7 +37,7 @@ class TestConfigManagerData(unittest.TestCase):
         It shouldn't append the data path to it.
         It shouldn't append the data path to it.
         """
         """
         abs_path = self.data_path + os.sep + "b10-config-imaginary.db"
         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(abs_path, data.db_filename)
         self.assertEqual(self.data_path, data.data_path)
         self.assertEqual(self.data_path, data.data_path)
 
 
@@ -88,7 +88,7 @@ class TestConfigManagerData(unittest.TestCase):
         self.assertEqual(cfd1, cfd2)
         self.assertEqual(cfd1, cfd2)
         cfd2.data['test'] = { 'a': [ 1, 2, 3]}
         cfd2.data['test'] = { 'a': [ 1, 2, 3]}
         self.assertNotEqual(cfd1, cfd2)
         self.assertNotEqual(cfd1, cfd2)
-        
+
 
 
 class TestConfigManager(unittest.TestCase):
 class TestConfigManager(unittest.TestCase):
 
 
@@ -198,8 +198,8 @@ class TestConfigManager(unittest.TestCase):
         self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
         self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
         config_spec = self.cm.get_config_spec('Spec2')
         config_spec = self.cm.get_config_spec('Spec2')
         self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
         self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
-        
-    
+
+
     def test_get_commands_spec(self):
     def test_get_commands_spec(self):
         commands_spec = self.cm.get_commands_spec()
         commands_spec = self.cm.get_commands_spec()
         self.assertEqual(commands_spec, {})
         self.assertEqual(commands_spec, {})
@@ -250,7 +250,7 @@ class TestConfigManager(unittest.TestCase):
     def test_write_config(self):
     def test_write_config(self):
         # tested in ConfigManagerData tests
         # tested in ConfigManagerData tests
         pass
         pass
-    
+
     def _handle_msg_helper(self, msg, expected_answer):
     def _handle_msg_helper(self, msg, expected_answer):
         answer = self.cm.handle_msg(msg)
         answer = self.cm.handle_msg(msg)
         self.assertEqual(expected_answer, answer)
         self.assertEqual(expected_answer, answer)
@@ -338,7 +338,7 @@ class TestConfigManager(unittest.TestCase):
         #                 self.fake_session.get_message(self.name, None))
         #                 self.fake_session.get_message(self.name, None))
         #self.assertEqual({'version': 1, 'TestModule': {'test': 124}}, self.cm.config.data)
         #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()]
                                   ["module_spec", self.spec.get_full_spec()]
                                 },
                                 },
                                 {'result': [0]})
                                 {'result': [0]})
@@ -359,7 +359,7 @@ class TestConfigManager(unittest.TestCase):
         #self.assertEqual({'commands_update': [ self.name, self.commands ] },
         #self.assertEqual({'commands_update': [ self.name, self.commands ] },
         #                 self.fake_session.get_message("Cmdctl", None))
         #                 self.fake_session.get_message("Cmdctl", None))
 
 
-        self._handle_msg_helper({ "command": 
+        self._handle_msg_helper({ "command":
                                   ["shutdown"]
                                   ["shutdown"]
                                 },
                                 },
                                 {'result': [0]})
                                 {'result': [0]})

+ 127 - 0
tests/lettuce/README

@@ -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 - 0
tests/lettuce/README.tutorial

@@ -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 - 0
tests/lettuce/configurations/example.org.config.orig

@@ -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 - 0
tests/lettuce/configurations/example2.org.config

@@ -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 - 0
tests/lettuce/configurations/no_db_file.config

@@ -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


BIN
tests/lettuce/data/example.org.sqlite3


+ 142 - 0
tests/lettuce/features/example.feature

@@ -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 - 0
tests/lettuce/features/terrain/bind10_control.py

@@ -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 - 0
tests/lettuce/features/terrain/querying.py

@@ -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 - 0
tests/lettuce/features/terrain/steps.py

@@ -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 - 0
tests/lettuce/features/terrain/terrain.py

@@ -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 - 0
tests/lettuce/setup_intree_bind10.sh.in

@@ -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