Browse Source

1. Add support to cmdctl.spec file, now there are three configurable items for cmdctl: 'key_file', 'cert_file' and 'accounts_file', all of them can be changed in
runtime. Also there is two command spported by cmdctl: shutdown and print_settings .
2. Refator some code of cmdctl and add corresponding test cases.
3. Rename module name from 'Cmd-Ctrld' to 'Cmdctl'.

git-svn-id: svn://bind10.isc.org/svn/bind10/branches/trac127@2253 e5f2f494-b856-4b98-b285-d166d9295462

Likun Zhang 15 years ago
parent
commit
8523458e6d

+ 1 - 0
configure.ac

@@ -428,6 +428,7 @@ AC_OUTPUT([src/bin/cfgmgr/b10-cfgmgr.py
            src/bin/cmdctl/cmdctl.py
            src/bin/cmdctl/run_b10-cmdctl.sh
            src/bin/cmdctl/tests/cmdctl_test
+           src/bin/cmdctl/cmdctl.spec.pre
            src/bin/xfrin/tests/xfrin_test
            src/bin/xfrin/xfrin.py
            src/bin/xfrin/xfrin.spec.pre

+ 1 - 1
src/bin/bind10/bind10.py.in

@@ -353,7 +353,7 @@ class BoB:
     def stop_all_processes(self):
         """Stop all processes."""
         cmd = { "command": ['shutdown']}
-        self.cc_session.group_sendmsg(cmd, 'Boss', 'Cmd-Ctrld')
+        self.cc_session.group_sendmsg(cmd, 'Boss', 'Cmdctl')
         self.cc_session.group_sendmsg(cmd, "Boss", "ConfigManager")
         self.cc_session.group_sendmsg(cmd, "Boss", "Auth")
         self.cc_session.group_sendmsg(cmd, "Boss", "Xfrout")

+ 4 - 1
src/bin/cmdctl/Makefile.am

@@ -19,7 +19,7 @@ b10_cmdctl_DATA += cmdctl.spec
 EXTRA_DIST = $(CMDCTL_CONFIGURATIONS)
 EXTRA_DIST += cmdctl.spec
 
-CLEANFILES=	b10-cmdctl cmdctl.pyc
+CLEANFILES=	b10-cmdctl cmdctl.pyc cmdctl.spec
 
 man_MANS = b10-cmdctl.8
 EXTRA_DIST += $(man_MANS) b10-cmdctl.xml
@@ -31,6 +31,9 @@ b10-cmdctl.8: b10-cmdctl.xml
 
 endif
 
+cmdctl.spec: cmdctl.spec.pre
+	$(SED) -e "s|@@SYSCONFDIR@@|$(sysconfdir)|" cmdctl.spec.pre >$@
+
 # TODO: does this need $$(DESTDIR) also?
 # this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
 b10-cmdctl: cmdctl.py

+ 2 - 5
src/bin/cmdctl/TODO

@@ -1,8 +1,5 @@
-. Refine code for b10-cmdctl.
-. Add value type check according module specification.
 . Add return code for RESTful API document of b10-cmdctl.
-. Add more unit tests for b10-cmdctl.
 . Update man page for b10-cmdctl?
+. Add check for the content of key/certificate file
+  (when cmdctl starts or is configured by bindctl).
 
-. Add id to each command, so the receiver knows if the response is what it wants.
-. Make cmdctl can be configured through bindctl.(after id is added)

+ 234 - 127
src/bin/cmdctl/cmdctl.py.in

@@ -41,6 +41,7 @@ import csv
 import random
 import time
 import signal
+from isc.config import ccsession
 from optparse import OptionParser, OptionValueError
 from hashlib import sha1
 try:
@@ -50,29 +51,22 @@ except ImportError:
 
 __version__ = 'BIND10'
 URL_PATTERN = re.compile('/([\w]+)(?:/([\w]+))?/?')
+MODULE_NAME = 'Cmdctl'
 
 # If B10_FROM_SOURCE is set in the environment, we use data files
 # from a directory relative to that, otherwise we use the ones
 # installed on the system
 if "B10_FROM_SOURCE" in os.environ:
     SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
-    SYSCONF_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
 else:
     PREFIX = "@prefix@"
     DATAROOTDIR = "@datarootdir@"
     SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
-    SYSCONF_PATH = "@sysconfdir@/@PACKAGE@".replace("${prefix}", PREFIX)
 SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
-USER_INFO_FILE = SYSCONF_PATH + os.sep + "cmdctl-accounts.csv"
-PRIVATE_KEY_FILE = SYSCONF_PATH + os.sep + "cmdctl-keyfile.pem"
-CERTIFICATE_FILE = SYSCONF_PATH + os.sep + "cmdctl-certfile.pem"
-        
+       
 class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
     '''https connection request handler.
-    Currently only GET and POST are supported.
-
-    '''
-
+    Currently only GET and POST are supported.  '''
     def do_GET(self):
         '''The client should send its session id in header with 
         the name 'cookie'
@@ -167,13 +161,13 @@ class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
         user_name = user_info.get('username')
         if not user_name:
             return False, ["need user name"]
-        if not self.server.user_infos.get(user_name):
+        if not self.server.get_user_info(user_name):
             return False, ["user doesn't exist"]
 
         user_pwd = user_info.get('password')
         if not user_pwd:
             return False, ["need password"]
-        local_info = self.server.user_infos.get(user_name)
+        local_info = self.server.get_user_info(user_name)
         pwd_hashval = sha1((user_pwd + local_info[1]).encode())
         if pwd_hashval.hexdigest() != local_info[0]:
             return False, ["password doesn't match"] 
@@ -214,65 +208,155 @@ class CommandControl():
     '''Get all modules' config data/specification from configmanager.
     receive command from client and resend it to proper module.
     '''
-
-    def __init__(self, verbose = False):
+    def __init__(self, httpserver, verbose = False):
+        '''When cmdctl get 'shutdown' command from boss, 
+        shutdown the outer httpserver. '''
         self._verbose = verbose
-        self.cc = isc.cc.Session()
-        self.cc.group_subscribe('Cmd-Ctrld')
-        self.module_spec = self.get_module_specification()
-        self.config_data = self.get_config_data()
+        self._httpserver = httpserver
+        self._lock = threading.Lock()
+        self._setup_session()
+        self.modules_spec = self._get_modules_specification()
+        self._config_data = self._get_config_data_from_config_manager()
+        self._serving = True
+        self._start_msg_handle_thread()
+
+    def _setup_session(self):
+        '''Setup the session for receving the commands
+        sent from other modules. There are two sessions 
+        for cmdctl, one(self.module_cc) is used for receiving 
+        commands sent from other modules, another one (self._cc) 
+        is used to send the command from Bindctl or other tools 
+        to proper modules.''' 
+        self._cc = isc.cc.Session()
+        self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
+                                              self.config_handler,
+                                              self.command_handler)
+        self._cmdctl_config_data = self._module_cc.get_full_config()
+        self._module_cc.start()
+    
+    def _accounts_file_check(self, filepath):
+        ''' Check whether the accounts file is valid, each row
+        should be a list with 3 items.'''
+        csvfile = None
+        errstr = None
+        try:
+            csvfile = open(filepath)
+            reader = csv.reader(csvfile)
+            for row in reader:
+                a = (row[0], row[1], row[2])
+        except (IOError, IndexError) as e:
+            errstr = 'Invalid accounts file: ' + str(e)
+        finally:
+            if csvfile:
+                csvfile.close()
+
+        return errstr
+
+    def _config_data_check(self, new_config):
+        ''' Check whether the new config data is valid or
+        not. '''
+        errstr = None
+        for key in new_config:
+            if key in ['key_file', 'cert_file']:
+                #TODO, only check whether the file exist,
+                # further check need to be done: eg. whether
+                # the private/certificate is valid.
+                path = new_config[key]
+                if not os.path.exists(path):
+                    errstr = "the file doesn't exist: " + path
+            elif key == 'accounts_file':
+                errstr = self._accounts_file_check(new_config[key])
+            else:
+                errstr = 'unknown config item: ' + key
+            
+            if errstr != None:
+                self.log_info('Fail to apply config data, ' + errstr) 
+                return ccsession.create_answer(1, errstr)
 
+        return ccsession.create_answer(0)
+
+    def config_handler(self, new_config):
+        answer = self._config_data_check(new_config)
+        rcode, val = ccsession.parse_answer(answer)
+        if rcode != 0:
+            return answer
+
+        with self._lock:
+            for key in new_config:
+                if key in self._cmdctl_config_data:
+                    self._cmdctl_config_data[key] = new_config[key]
+        return answer
+
+    def command_handler(self, command, args):
+        answer = ccsession.create_answer(0)
+        if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
+            self.modules_spec[arg[0]] = arg[1]
+
+        elif command == ccsession.COMMAND_SHUTDOWN:
+            self._httpserver.shutdown()
+            self._serving = False
+
+        elif command == 'print_settings':
+            answer = ccsession.create_answer(0, self._cmdctl_config_data)
+        else:
+            answer = ccsession.create_answer(1, 'unknown command: ' + command)
+
+        return answer
+
+    def _start_msg_handle_thread(self):
+        ''' Start one thread to handle received message from msgq.'''
+        td = threading.Thread(target=self._handle_msg_from_msgq)
+        td.daemon = True
+        td.start()
+
+    def _handle_msg_from_msgq(self):
+        '''Process all the received commands with module session. '''
+        while self._serving:
+            self._module_cc.check_command() 
+ 
     def _parse_command_result(self, rcode, reply):
         '''Ignore the error reason when command rcode isn't 0, '''
         if rcode != 0:
             return {}
         return reply
 
-    def get_config_data(self):
+    def _get_config_data_from_config_manager(self):
         '''Get config data for all modules from configmanager '''
-        rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_CONFIG)
+        rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_CONFIG)
         return self._parse_command_result(rcode, reply)
 
-
-    def update_config_data(self, module_name, command_name):
+    def _update_config_data(self, module_name, command_name):
         '''Get lastest config data for all modules from configmanager '''
-        if module_name == 'ConfigManager' and command_name == isc.config.ccsession.COMMAND_SET_CONFIG:
-            self.config_data = self.get_config_data()
+        if module_name == 'ConfigManager' and command_name == ccsession.COMMAND_SET_CONFIG:
+            data = self._get_config_data_from_config_manager()
+            with self._lock:
+                self._config_data = data
+
+    def get_config_data(self):
+        with self._lock:
+            data = self._config_data
+        return data
 
-    def get_module_specification(self):
-        rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_MODULE_SPEC)
+    def _get_modules_specification(self):
+        '''Get all the modules' specification files. '''
+        rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
         return self._parse_command_result(rcode, reply)
 
-    def handle_recv_msg(self):
-        '''Handle received message, if 'shutdown' is received, return False'''
-        (message, env) = self.cc.group_recvmsg(True)
-        command, arg = isc.config.ccsession.parse_command(message)
-        while command:
-            if command == isc.config.ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
-                self.module_spec[arg[0]] = arg[1]
-            elif command == isc.config.ccsession.COMMAND_SHUTDOWN:
-                return False
-            (message, env) = self.cc.group_recvmsg(True)
-            command, arg = isc.config.ccsession.parse_command(message)
-        
-        return True
-    
     def send_command_with_check(self, module_name, command_name, params = None):
         '''Before send the command to modules, check if module_name, command_name
         parameters are legal according the spec file of the module.
-        Return rcode, dict.
+        Return rcode, dict. TODO, the rcode should be defined properly.
         rcode = 0: dict is the correct returned value.
         rcode > 0: dict is : { 'error' : 'error reason' }
         '''
-
         # core module ConfigManager does not have a specification file
         if module_name == 'ConfigManager':
             return self.send_command(module_name, command_name, params)
 
-        if module_name not in self.module_spec.keys():
+        if module_name not in self.modules_spec.keys():
             return 1, {'error' : 'unknown module'}
        
-        spec_obj = isc.config.module_spec.ModuleSpec(self.module_spec[module_name], False)
+        spec_obj = isc.config.module_spec.ModuleSpec(self.modules_spec[module_name], False)
         errors = []
         if not spec_obj.validate_command(command_name, params, errors):
             return 1, {'error': errors[0]}
@@ -281,123 +365,146 @@ class CommandControl():
 
     def send_command(self, module_name, command_name, params = None):
         '''Send the command from bindctl to proper module. '''
-        
-        errstr = 'no error'
+        errstr = 'unknown error'
         if self._verbose:
-            self.log_info('[b10-cmdctl] send command \'%s\' to %s\n' %(command_name, module_name))
-        try:
-            msg = isc.config.ccsession.create_command(command_name, params)
-            seq = self.cc.group_sendmsg(msg, module_name)
+            self.log_info('Begin send command \'%s\' to %s\n' %(command_name, module_name))
+
+        if module_name == MODULE_NAME:
+            # Process the command sent to cmdctl directly. 
+            answer = self.command_handler(command_name, params)
+        else:
+            msg = ccsession.create_command(command_name, params)
+            seq = self._cc.group_sendmsg(msg, module_name)
             #TODO, it may be blocked, msqg need to add a new interface waiting in timeout.
-            answer, env = self.cc.group_recvmsg(False, seq)
-            if answer:
-                try:
-                    rcode, arg = isc.config.ccsession.parse_answer(answer)
-                    if rcode == 0:
-                        self.update_config_data(module_name, command_name)
-                        if arg != None:
-                            return rcode, arg
-                        else:
-                            return rcode, {}
+            answer, env = self._cc.group_recvmsg(False, seq)
+
+        if answer:
+            try:
+                rcode, arg = ccsession.parse_answer(answer)
+                if rcode == 0:
+                    self._update_config_data(module_name, command_name)
+                    if arg != None:
+                        return rcode, arg
                     else:
-                        # todo: exception
-                        errstr = str(answer['result'][1])
-                except isc.config.ccsession.ModuleCCSessionError as mcse:
-                    errstr = str("Error in ccsession answer:") + str(mcse)
-                    self.log_info(answer)
-        except Exception as e:
-            errstr = str(e)
-            self.log_info('\'%s\':[b10-cmdctl] fail send command \'%s\' to %s\n' % (e, command_name, module_name))
+                        return rcode, {}
+                else:
+                    # TODO: exception
+                    errstr = str(answer['result'][1])
+            except ccsession.ModuleCCSessionError as mcse:
+                errstr = str("Error in ccsession answer:") + str(mcse)
+                self.log_info(errstr)
         
         return 1, {'error': errstr}
     
     def log_info(self, msg):
         sys.stdout.write("[b10-cmdctl] %s\n" % str(msg))
 
+    def get_cmdctl_config_data(self):
+        ''' If running in source code tree, use keyfile, certificate
+        and user accounts file in source code. '''
+        if "B10_FROM_SOURCE" in os.environ:
+            sysconf_path = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl/"
+            accountsfile  = sysconf_path + "cmdctl-accounts.csv"
+            keyfile = sysconf_path + "cmdctl-keyfile.pem"
+            certfile = sysconf_path + "cmdctl-certfile.pem"
+            return (keyfile, certfile, accountsfile)
+
+        with self._lock:
+            keyfile = self._cmdctl_config_data.get('key_file')
+            certfile = self._cmdctl_config_data.get('cert_file')
+            accountsfile = self._cmdctl_config_data.get('accounts_file')
+
+        return (keyfile, certfile, accountsfile)
+
 class SecureHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
     '''Make the server address can be reused.'''
     allow_reuse_address = True
 
-    def __init__(self, server_address, RequestHandlerClass, idle_timeout = 1200, verbose = False):
+    def __init__(self, server_address, RequestHandlerClass, 
+                 CommandControlClass,
+                 idle_timeout = 1200, verbose = False):
         '''idle_timeout: the max idle time for login'''
         http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
         self.user_sessions = {}
         self.idle_timeout = idle_timeout
-        self.cmdctrl = CommandControl()
-        self.__is_shut_down = threading.Event()
-        self.__serving = False
+        self.cmdctl = CommandControlClass(self, verbose)
         self._verbose = verbose
-        self.user_infos = {}
-        self._read_user_info()
+        self._lock = threading.Lock()
+        self._user_infos = {}
+        self._accounts_file = None
+
+    def _create_user_info(self, accounts_file):
+        '''Read all user's name and its' salt, hashed password 
+        from accounts file.'''
+        if (self._accounts_file == accounts_file) and (len(self._user_infos) > 0): 
+            return
+
+        with self._lock:
+            self._user_infos = {}
+            csvfile = None
+            try:
+                csvfile = open(accounts_file)
+                reader = csv.reader(csvfile)
+                for row in reader:
+                    self._user_infos[row[0]] = [row[1], row[2]]
+            except (IOError, IndexError) as e:
+                self.log_info("Fail to read user database:'%s'\n" % e)                
+            finally:
+                if csvfile:
+                    csvfile.close()
+
+        self._accounts_file = accounts_file
+        if len(self._user_infos) == 0:
+            self.log_info("Fail to get user information, will deny any user")                
+         
+    def get_user_info(self, username):
+        '''Get user's salt and hashed string. If the user
+        doesn't exist, return None, or else, the list 
+        [salt, hashed password] will be returned.'''
+        with self._lock:
+            info = self._user_infos.get(username)
+        return info
 
-    def _read_user_info(self):
-        '''Read all user's name and its' password from csv file.'''
-        csvfile = None
-        try:
-            csvfile = open(USER_INFO_FILE)
-            reader = csv.reader(csvfile)
-            for row in reader:
-                self.user_infos[row[0]] = [row[1], row[2]]
-        except Exception as e:
-            self.log_info('[b10-cmdctl] Fail to read user information :\'%s\'\n' % e)                
-        finally:
-            if csvfile:
-                csvfile.close()
-        
     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()
         
-    def get_request(self):
-        '''Get client request socket and wrap it in SSL context. '''
-        newsocket, fromaddr = self.socket.accept()
+    def _wrap_socket_in_ssl_context(self, sock, key, cert):
+        # TODO, report error when key/certificate file doesn't exist
         try:
-            connstream = ssl.wrap_socket(newsocket,
-                                     server_side = True,
-                                     certfile = CERTIFICATE_FILE,
-                                     keyfile = PRIVATE_KEY_FILE,
-                                     ssl_version = ssl.PROTOCOL_SSLv23)
-            return (connstream, fromaddr)
+            ssl_sock = ssl.wrap_socket(sock,
+                                      server_side = True,
+                                      certfile = cert,
+                                      keyfile = key,
+                                      ssl_version = ssl.PROTOCOL_SSLv23)
+            return ssl_sock 
         except ssl.SSLError as e :
-            self.log_info('[b10-cmdctl] deny client\'s invalid connection:\'%s\'\n' % e)
-            self.close_request(newsocket)
+            self.log_info("Deny client's invalid connection:%s\n" % e)
+            self.close_request(sock)
             # raise socket error to finish the request
             raise socket.error
-            
+
+    def get_request(self):
+        '''Get client request socket and wrap it in SSL context. '''
+        key, cert, account_file = self.cmdctl.get_cmdctl_config_data()
+        self._create_user_info(account_file)
+        newsocket, fromaddr = self.socket.accept()
+        ssl_sock = self._wrap_socket_in_ssl_context(newsocket, key, cert)
+        return (ssl_sock, fromaddr)
 
     def get_reply_data_for_GET(self, id, module):
         '''Currently only support the following three url GET request '''
         rcode, reply = http.client.NO_CONTENT, []        
         if not module:
             if id == 'config_data':
-               rcode, reply = http.client.OK, self.cmdctrl.config_data
+               rcode, reply = http.client.OK, self.cmdctl.get_config_data()
             elif id == 'module_spec':
-                rcode, reply = http.client.OK, self.cmdctrl.module_spec
+                rcode, reply = http.client.OK, self.cmdctl.modules_spec
         
         return rcode, reply 
 
-        
-    def serve_forever(self, poll_interval = 0.5):
-        '''Start cmdctl as one tcp server. '''
-        self.__serving = True
-        self.__is_shut_down.clear()
-        while self.__serving:
-            if not self.cmdctrl.handle_recv_msg():
-                break
-
-            r, w, e = select.select([self], [], [], poll_interval)
-            if r:
-                self._handle_request_noblock()
-
-        self.__is_shut_down.set()
-    
-    def shutdown(self):
-        self.__serving = False
-        self.__is_shut_down.wait()
-
-
     def send_command_to_module(self, module_name, command_name, params):
-        return self.cmdctrl.send_command_with_check(module_name, command_name, params)
+        return self.cmdctl.send_command_with_check(module_name, command_name, params)
    
     def log_info(self, msg):
         sys.stdout.write("[b10-cmdctl] %s\n" % str(msg))
@@ -417,7 +524,8 @@ def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
     ''' Start cmdctl as one https server. '''
     if verbose:
         sys.stdout.write("[b10-cmdctl] starting on %s port:%d\n" %(addr, port))
-    httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler, idle_timeout, verbose)
+    httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler, 
+                             CommandControl, idle_timeout, verbose)
     httpd.serve_forever()
 
 def check_port(option, opt_str, value, parser):
@@ -453,13 +561,12 @@ def set_cmd_options(parser):
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
             help="display more about what is going on")
 
-
 if __name__ == '__main__':
     try:
+        set_signal_handler()
         parser = OptionParser(version = __version__)
         set_cmd_options(parser)
         (options, args) = parser.parse_args()
-        set_signal_handler()
         run(options.addr, options.port, options.idle_timeout, options.verbose)
     except isc.cc.SessionError as se:
         sys.stderr.write("[b10-cmdctl] Error creating b10-cmdctl, "

+ 5 - 15
src/bin/cmdctl/cmdctl.spec

@@ -7,42 +7,32 @@
         "item_name": "key_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-keyfile.pem'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-keyfile.pem'
       },
       {
         "item_name": "cert_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-certfile.pem'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-certfile.pem'
       },
       {
         "item_name": "accounts_file",
         "item_type": "string",
         "item_optional": False,
-        "item_default": 'cmdctl-accounts.csv'
+        "item_default": '@@SYSCONFDIR@@/@PACKAGE@/cmdctl-accounts.csv'
       }
     ],
     "commands": [
       {
-        "command_name": "print_message",
-        "command_description": "Print the given message to stdout",
-        "command_args": [ {
-          "item_name": "message",
-          "item_type": "string",
-          "item_optional": False,
-          "item_default": ""
-        } ]
-      },
-      {
         "command_name": "print_settings",
         "command_description": "Print some_string and some_int to stdout",
         "command_args": []
       },
       {
         "command_name": "shutdown",
-        "command_description": "Shut down cmdctl",
+        "command_description": "shutdown cmdctl",
         "command_args": []
-      }
+      },
     ]
   }
 }

+ 161 - 14
src/bin/cmdctl/tests/cmdctl_test.py

@@ -16,6 +16,7 @@
 
 import unittest
 import socket
+import tempfile
 from cmdctl import *
 
 # Rewrite the class for unittest.
@@ -42,17 +43,20 @@ class MySecureHTTPRequestHandler(SecureHTTPRequestHandler):
         os.remove('tmp.file')
     
 
-class MySecureHTTPServer(SecureHTTPServer):
+class FakeSecureHTTPServer(SecureHTTPServer):
     def __init__(self):
         self.user_sessions = {}
+        self.cmdctl = FakeCommandControlForTestRequestHandler()
+        self._verbose = True 
+        self._user_infos = {}
         self.idle_timeout = 1200
-        self.cmdctrl = MyCommandControl()
-        self._verbose = False
+        self._lock = threading.Lock()
 
-class MyCommandControl(CommandControl):
+class FakeCommandControlForTestRequestHandler(CommandControl):
     def __init__(self):
-        self.config_data = {}
-        self.module_spec = {}
+        self._config_data = {}
+        self.modules_spec = {}
+        self._lock = threading.Lock()
 
     def send_command(self, mod, cmd, param):
         return 0, {}
@@ -60,14 +64,17 @@ class MyCommandControl(CommandControl):
 
 class TestSecureHTTPRequestHandler(unittest.TestCase):
     def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
         self.handler = MySecureHTTPRequestHandler()
-        self.handler.server = MySecureHTTPServer()
+        self.handler.server = FakeSecureHTTPServer()
         self.handler.server.user_sessions = {}
-        self.handler.server.user_infos = {}
+        self.handler.server._user_infos = {}
         self.handler.headers = {}
         self.handler.rfile = open("check.tmp", 'w+b')
 
     def tearDown(self):
+        sys.stdout = self.old_stdout
         self.handler.rfile.close()
         os.remove('check.tmp')
 
@@ -154,7 +161,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.headers['Content-Length'] = len
         self.handler.rfile.seek(0, 0)
 
-        self.handler.server.user_infos['root'] = ['aa', 'aaa']
+        self.handler.server._user_infos['root'] = ['aa', 'aaa']
         ret, msg = self.handler._check_user_name_and_pwd()
         self.assertTrue(ret == False)
         self.assertEqual(msg, ['password doesn\'t match'])
@@ -185,7 +192,7 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
         self.handler.headers['Content-Length'] = len
         self.handler.rfile.seek(0, 0)
 
-        self.handler.server.user_infos['root'] = ['aa', 'aaa']
+        self.handler.server._user_infos['root'] = ['aa', 'aaa']
         ret, msg = self.handler._check_user_name_and_pwd()
         self.assertTrue(ret == False)
         self.assertEqual(msg, ['need password'])
@@ -247,8 +254,8 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
 
         self.handler.rfile.seek(0, 0)
         self.handler.path = '/module/command'
-        self.handler.server.cmdctrl.module_spec = {}
-        self.handler.server.cmdctrl.module_spec['module'] = self._gen_module_spec()
+        self.handler.server.cmdctl.modules_spec = {}
+        self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
         rcode, reply = self.handler._handle_post_request()
         self.assertEqual(http.client.OK, rcode)
 
@@ -259,10 +266,150 @@ class TestSecureHTTPRequestHandler(unittest.TestCase):
 
         self.handler.rfile.seek(0, 0)
         self.handler.path = '/module/command'
-        self.handler.server.cmdctrl.module_spec = {}
-        self.handler.server.cmdctrl.module_spec['module'] = self._gen_module_spec()
+        self.handler.server.cmdctl.modules_spec = {}
+        self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
         rcode, reply = self.handler._handle_post_request()
         self.assertEqual(http.client.BAD_REQUEST, rcode)
 
+class MyCommandControl(CommandControl):
+    def _get_modules_specification(self):
+        return {}
+
+    def _get_config_data_from_config_manager(self):
+        return {}
+
+    def _setup_session(self):
+        module_spec = isc.config.module_spec_from_file(SPECFILE_LOCATION)
+        config = isc.config.config_data.ConfigData(module_spec)
+        self._cmdctl_config_data = config.get_full_config()
+
+    def _handle_msg_from_msgq(self): 
+        pass
+
+class TestCommandControl(unittest.TestCase):
+
+    def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
+        self.cmdctl = MyCommandControl(None, True)
+   
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+
+    def _check_config(self, cmdctl):
+        key, cert, account = cmdctl.get_cmdctl_config_data()
+        self.assertIsNotNone(key)
+        self.assertIsNotNone(cert)
+        self.assertIsNotNone(account)
+
+    def test_get_cmdctl_config_data(self):
+        old_env = os.environ
+        if 'B10_FROM_SOURCE' in os.environ:
+            del os.environ['B10_FROM_SOURCE']
+        self.cmdctl.get_cmdctl_config_data() 
+        self._check_config(self.cmdctl)
+        os.environ = old_env
+
+        old_env = os.environ
+        os.environ['B10_FROM_SOURCE'] = '../'
+        self._check_config(self.cmdctl)
+        os.environ = old_env
+    
+    def test_parse_command_result(self):
+        self.assertEqual({}, self.cmdctl._parse_command_result(1, {'error' : 1}))
+        self.assertEqual({'a': 1}, self.cmdctl._parse_command_result(0, {'a' : 1}))
+
+    def _check_answer(self, answer, rcode_, msg_):
+        rcode, msg = ccsession.parse_answer(answer)
+        self.assertEqual(rcode, rcode_)
+        self.assertEqual(msg, msg_)
+
+    def test_command_handler(self):
+        answer = self.cmdctl.command_handler('unknown-command', None)
+        self._check_answer(answer, 1, 'unknown command: unknown-command')
+
+        answer = self.cmdctl.command_handler('print_settings', None)
+        self._check_answer(answer, 0, None) 
+
+    def test_check_config_handler(self):
+        answer = self.cmdctl.config_handler({'non-exist': 123})
+        self._check_answer(answer, 1, 'unknown config item: non-exist')
+
+        old_env = os.environ
+        os.environ['B10_FROM_SOURCE'] = '../'
+        self._check_config(self.cmdctl)
+        answer = self.cmdctl.config_handler({'key_file' : self.cmdctl._cmdctl_config_data['key_file']})
+        self._check_answer(answer, 0, None)
+
+        answer = self.cmdctl.config_handler({'cert_file' : self.cmdctl._cmdctl_config_data['cert_file']})
+        self._check_answer(answer, 0, None)
+
+        answer = self.cmdctl.config_handler({'accounts_file' : self.cmdctl._cmdctl_config_data['accounts_file']})
+        self._check_answer(answer, 0, None)
+        os.environ = old_env
+
+        answer = self.cmdctl.config_handler({'key_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, "the file doesn't exist: /user/non-exist_folder")
+
+        answer = self.cmdctl.config_handler({'cert_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, "the file doesn't exist: /user/non-exist_folder")
+
+        answer = self.cmdctl.config_handler({'accounts_file': '/user/non-exist_folder'})
+        self._check_answer(answer, 1, 
+                "Invalid accounts file: [Errno 2] No such file or directory: '/user/non-exist_folder'")
+
+        # Test with invalid accounts file
+        file_name = 'tmp.account.file'
+        temp_file = open(file_name, 'w')
+        writer = csv.writer(temp_file)
+        writer.writerow(['a', 'b'])
+        temp_file.close()
+        answer = self.cmdctl.config_handler({'accounts_file': file_name})
+        self._check_answer(answer, 1, "Invalid accounts file: list index out of range")
+        os.remove(file_name)
+    
+    def test_send_command(self):
+        rcode, value = self.cmdctl.send_command(MODULE_NAME, 'print_settings', None)
+        self.assertEqual(rcode, 0)
+
+class MySecureHTTPServer(SecureHTTPServer):
+    def server_bind(self):
+        pass
+
+class TestSecureHTTPServer(unittest.TestCase):
+    def setUp(self):
+        self.old_stdout = sys.stdout
+        sys.stdout = open(os.devnull, 'w')
+        self.server = MySecureHTTPServer(('localhost', 8080), 
+                                         MySecureHTTPRequestHandler,
+                                         MyCommandControl, verbose=True)
+
+    def tearDown(self):
+        sys.stdout = self.old_stdout
+
+    def test_create_user_info(self):
+        self.server._create_user_info('/local/not-exist')
+        self.assertEqual(0, len(self.server._user_infos))
+
+        self.server._create_user_info('../cmdctl-accounts.csv')
+        self.assertEqual(1, len(self.server._user_infos))
+        self.assertTrue('root' in self.server._user_infos)
+
+    def test_wrap_sock_in_ssl_context(self):
+        sock = socket.socket()
+        self.server._wrap_socket_in_ssl_context(sock, 
+                                          '../cmdctl-keyfile',
+                                          '../cmdctl-certfile')
+
+class TestFuncNotInClass(unittest.TestCase):
+    def test_check_port(self):
+        self.assertRaises(OptionValueError, check_port, None, 'port', -1, None)        
+        self.assertRaises(OptionValueError, check_port, None, 'port', 65536, None)        
+        self.assertRaises(OptionValueError, check_addr, None, 'ipstr', 'a.b.d', None)        
+        self.assertRaises(OptionValueError, check_addr, None, 'ipstr', '1::0:a.b', None)        
+
+
 if __name__== "__main__":
     unittest.main()
+
+

+ 1 - 1
src/lib/python/isc/config/cfgmgr.py

@@ -347,7 +347,7 @@ class ConfigManager:
         # passes both specification and commands at once
         spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
                                                [ spec.get_module_name(), spec.get_full_spec() ])
-        self.cc.group_sendmsg(spec_update, "Cmd-Ctrld")
+        self.cc.group_sendmsg(spec_update, "Cmdctl")
         return ccsession.create_answer(0)
 
     def handle_msg(self, msg):

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

@@ -271,9 +271,9 @@ class TestConfigManager(unittest.TestCase):
         # the name here is actually wrong (and hardcoded), but needed in the current version
         # TODO: fix that
         #self.assertEqual({'specification_update': [ self.name, self.spec ] },
-        #                 self.fake_session.get_message("Cmd-Ctrld", None))
+        #                 self.fake_session.get_message("Cmdctl", None))
         #self.assertEqual({'commands_update': [ self.name, self.commands ] },
-        #                 self.fake_session.get_message("Cmd-Ctrld", None))
+        #                 self.fake_session.get_message("Cmdctl", None))
 
         self._handle_msg_helper({ "command": 
                                   ["shutdown"]