Browse Source

added a data.py class with a few classes and function usable by configuration handlers. bigtool now has a fixed config 'module' which takes configuration commands. The completion function completes configuration identifiers, though i haven't been able to get rid of the space yet, which would not always be ideal

config commands are kind of based on the message i sent last week to -dev; set,unset,add (to list), remove (from list), revert, and commit

changed configuration is stored, but not sent to and used by parkinglot etc. yet


git-svn-id: svn://bind10.isc.org/svn/bind10/branches/jelte-datadef@343 e5f2f494-b856-4b98-b285-d166d9295462
Jelte Jansen 15 years ago
parent
commit
57979d6e09

+ 47 - 4
src/bin/bigtool/run_bigtool.py

@@ -28,7 +28,7 @@ def _prepare_fake_data(bigtool):
     bigtool.add_module_info(zone_module)
     bigtool.add_module_info(boss_module)
 
-def prepare_data_module(bigtool, module_name, module_commands):
+def prepare_module_commands(bigtool, module_name, module_commands):
     module = ModuleInfo(name = module_name,
                         desc = "same here")
     for command in module_commands:
@@ -45,10 +45,52 @@ def prepare_data_module(bigtool, module_name, module_commands):
         module.add_command(cmd)
     bigtool.add_module_info(module)
 
-def prepare_data(bigtool, command_spec):
+def prepare_commands(bigtool, command_spec):
     for module_name in command_spec.keys():
-        prepare_data_module(bigtool, module_name, command_spec[module_name])
+        prepare_module_commands(bigtool, module_name, command_spec[module_name])
+
+def prepare_config_commands(bigtool):
+    module = ModuleInfo(name = "config", desc = "Configuration commands")
+    cmd = CommandInfo(name = "show", desc = "Show configuration", need_inst_param = False)
+    param = ParamInfo(name = "identifier", type = "string", optional=True)
+    cmd.add_param(param)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "add", desc = "Add entry to configuration list", need_inst_param = False)
+    param = ParamInfo(name = "identifier", type = "string", optional=True)
+    cmd.add_param(param)
+    param = ParamInfo(name = "value", type = "string", optional=True)
+    cmd.add_param(param)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list", need_inst_param = False)
+    param = ParamInfo(name = "identifier", type = "string", optional=True)
+    cmd.add_param(param)
+    param = ParamInfo(name = "value", type = "string", optional=True)
+    cmd.add_param(param)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "set", desc = "Set a configuration value", need_inst_param = False)
+    param = ParamInfo(name = "identifier", type = "string", optional=True)
+    cmd.add_param(param)
+    param = ParamInfo(name = "value", type = "string", optional=True)
+    cmd.add_param(param)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "unset", desc = "Unset a configuration value", need_inst_param = False)
+    param = ParamInfo(name = "identifier", type = "string", optional=True)
+    cmd.add_param(param)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "revert", desc = "Revert all local changes", need_inst_param = False)
+    module.add_command(cmd)
+
+    cmd = CommandInfo(name = "commit", desc = "Commit all local changes", need_inst_param = False)
+    module.add_command(cmd)
+
+    bigtool.add_module_info(module)
     
+
 if __name__ == '__main__':
     try:
         cc = ISC.CC.Session()
@@ -59,7 +101,8 @@ if __name__ == '__main__':
         tool = BigTool(cc)
         cc.group_sendmsg({ "command": ["get_commands"] }, "ConfigManager")
         command_spec, env =  cc.group_recvmsg(False)
-        prepare_data(tool, command_spec["result"])
+        prepare_commands(tool, command_spec["result"])
+        prepare_config_commands(tool)
         _prepare_fake_data(tool)   
         tool.cmdloop()
     except ISC.CC.SessionError:

+ 49 - 4
src/lib/bigtool/bigtool.py

@@ -7,6 +7,7 @@ from moduleinfo import ParamInfo
 from command import BigToolCmd
 from xml.dom import minidom
 import ISC
+import ISC.CC.data
 
 try:
     from collections import OrderedDict
@@ -34,7 +35,7 @@ class BigTool(Cmd):
         self.modules = OrderedDict()
         self.add_module_info(ModuleInfo("help", desc = "Get help for bigtool"))
         self.cc = session
-
+        self.config_data = ISC.CC.data.UIConfigData("", session)
 
     def validate_cmd(self, cmd):
         if not cmd.module in self.modules:
@@ -85,13 +86,15 @@ class BigTool(Cmd):
             for name in manda_params:
                 if not name in params and not param_nr in params:
                     raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
-                              
+                
                 param_nr += 1
 
     def _handle_cmd(self, cmd):
         #to do, consist xml package and send to bind10
         if cmd.command == "help" or ("help" in cmd.params.keys()):
             self._handle_help(cmd)
+        elif cmd.module == "config":
+            self.apply_config_cmd(cmd)
         else:
             self.apply_cmd(cmd)
 
@@ -141,6 +144,11 @@ class BigTool(Cmd):
                 else:                       
                     hints = self._get_param_startswith(cmd.module, cmd.command,
                                                        text)
+                    if cmd.module == "config":
+                        # grm text has been stripped of slashes...
+                        my_text = cur_line.rpartition(" ")[2]
+                        list = self.config_data.config.get_item_list(my_text.rpartition("/")[0])
+                        hints.extend([val for val in list if val.startswith(text)])
             except CmdModuleNameFormatError:
                 if not text:
                     hints = list(self.modules.keys())
@@ -160,9 +168,9 @@ class BigTool(Cmd):
 
             except BigToolException:
                 hints = []
-            
+
             self.hint = hints
-            self._append_space_to_hint()
+            #self._append_space_to_hint()
 
         if state < len(self.hint):
             return self.hint[state]
@@ -240,6 +248,43 @@ class BigTool(Cmd):
             self.modules[cmd.module].command_help(cmd.command)
 
 
+    def apply_config_cmd(self, cmd):
+        identifier = ""
+        try:
+            if 'identifier' in cmd.params:
+                identifier = cmd.params['identifier']
+            if cmd.command == "show":
+                values = self.config_data.get_value_maps(identifier)
+                for value_map in values:
+                    line = value_map['name']
+                    if value_map['type'] in [ 'module', 'map', 'list' ]:
+                        line += "/"
+                    else:
+                        line += ":\t" + str(value_map['value'])
+                    line += "\t" + value_map['type']
+                    line += "\t"
+                    if value_map['default']:
+                        line += "(default)"
+                    if value_map['modified']:
+                        line += "(modified)"
+                    print(line)
+            elif cmd.command == "add":
+                self.config_data.add(identifier, cmd.params['value'])
+            elif cmd.command == "remove":
+                self.config_data.remove(identifier, cmd.params['value'])
+            elif cmd.command == "set":
+                self.config_data.set(identifier, cmd.params['value'])
+            elif cmd.command == "unset":
+                self.config_data.unset(identifier)
+            elif cmd.command == "revert":
+                self.config_data.revert()
+            elif cmd.command == "commit":
+                self.config_data.commit(self.cc)
+        except ISC.CC.data.DataTypeError as dte:
+            print("Error: " + str(dte))
+        except ISC.CC.data.DataNotFoundError as dnfe:
+            print("Error: " + identifier + " not found")
+
     def apply_cmd(self, cmd):
         if not self.cc:
             return

+ 45 - 2
src/lib/bind-cfgd/python/bind-cfgd.py

@@ -1,10 +1,12 @@
 import ISC
 import pickle
 import signal
-    
+from ISC.CC import data
+
 class ConfigData:
     def __init__(self):
         self.zones = {}
+        self.data = {}
 
     def add_zone(self, zone_name, zone_file):
         self.zones[zone_name] = zone_file
@@ -85,6 +87,47 @@ class ConfigManager:
             try:
                 if cmd[0] == "get_commands":
                     answer["result"] = self.commands
+                elif cmd[0] == "get_data_spec":
+                    if len(cmd) > 1 and cmd[1] != "":
+                        try:
+                            answer["result"] = [0, self.data_definitions[cmd[1]]]
+                        except KeyError as ke:
+                            answer["result"] = [1, "No specification for module " + cmd[1]]
+                    else:
+                        answer["result"] = [0, self.data_definitions]
+                elif cmd[0] == "get_config":
+                    # we may not have any configuration here
+                    conf_part = None
+                    if len(cmd) > 1:
+                        try:
+                            conf_part = data.find(self.config.data, cmd[1])
+                        except data.DataNotFoundError as dnfe:
+                            pass
+                    else:
+                        conf_part = self.config.data
+                    answer["result"] = [ 0, conf_part ]
+                elif cmd[0] == "set_config":
+                    print("[XX] cmd len: " + str(len(cmd)))
+                    print("[XX] cmd 0: " + str(cmd[0]))
+                    print("[XX] cmd 1: " + str(cmd[1]))
+                    print("[XX] cmd 2: " + str(cmd[2]))
+                    if len(cmd) == 3:
+                        # todo: use api (and check types?)
+                        if cmd[1] != "":
+                            conf_part = data.find_no_exc(self.config.data, cmd[1])
+                            if not conf_part:
+                                conf_part = data.set(self.config.data, cmd[1], "")
+                        else:
+                            conf_part = self.config.data
+                        conf_part.update(cmd[2])
+                        # send out changed info
+                        answer["result"] = [ 0 ]
+                    elif len(cmd) == 2:
+                        self.config.data.update(cmd[1])
+                        # send out changed info
+                        answer["result"] = [ 0 ]
+                    else:
+                        answer["result"] = [ 1, "Wrong number of arguments" ]
                 elif cmd[0] == "zone" and cmd[1] == "add":
                     self.add_zone(cmd[2])
                     answer["result"] = [ 0 ]
@@ -101,7 +144,7 @@ class ConfigManager:
                     answer["result"] = [ 1, "Unknown command: " + str(cmd) ]
             except IndexError as ie:
                 print("missing argument")
-                answer["result"] = [ 1, "Missing argument in command" ]
+                answer["result"] = [ 1, "Missing argument in command: " + str(ie) ]
         elif "data_specification" in msg:
             # todo: validate? (no direct access to spec as
             spec = msg["data_specification"]

+ 328 - 0
src/lib/cc/python/ISC/CC/data.py

@@ -0,0 +1,328 @@
+# data, data_definition, config_data, module_config_data and ui_config_data classes
+# we might want to split these up :)
+import ast
+
+class DataNotFoundError(Exception): pass
+class DataTypeError(Exception): pass
+
+def find(element, identifier):
+    """Returns the subelement in the given data element, raises DataNotFoundError if not found"""
+    id_parts = identifier.split("/")
+    id_parts[:] = (value for value in id_parts if value != "")
+    cur_el = element
+    for id in id_parts:
+        if type(cur_el) == dict and id in cur_el.keys():
+            cur_el = cur_el[id]
+        else:
+            raise DataNotFoundError(identifier + " in " + str(element))
+    return cur_el
+
+def set(element, identifier, value):
+    id_parts = identifier.split("/")
+    id_parts[:] = (value for value in id_parts if value != "")
+    cur_el = element
+    for id in id_parts[:-1]:
+        if id in cur_el.keys():
+            cur_el = cur_el[id]
+        else:
+            cur_el[id] = {}
+            cur_el = cur_el[id]
+    cur_el[id_parts[-1]] = value
+    return element
+
+def unset(element, identifier):
+    id_parts = identifier.split("/")
+    id_parts[:] = (value for value in id_parts if value != "")
+    cur_el = element
+    for id in id_parts[:-1]:
+        if id in cur_el.keys():
+            cur_el = cur_el[id]
+        else:
+            cur_el[id] = {}
+            cur_el = cur_el[id]
+    cur_el[id_parts[-1]] = None
+    return element
+
+def find_no_exc(element, identifier):
+    """Returns the subelement in the given data element, returns None if not found"""
+    id_parts = identifier.split("/")
+    id_parts[:] = (value for value in id_parts if value != "")
+    cur_el = element
+    for id in id_parts:
+        if type(cur_el) == dict and id in cur_el.keys():
+            cur_el = cur_el[id]
+        else:
+            return None
+    return cur_el
+
+def find_spec(element, identifier):
+    """find the data definition for the given identifier
+       returns either a map with 'item_name' etc, or a list of those"""
+    id_parts = identifier.split("/")
+    id_parts[:] = (value for value in id_parts if value != "")
+    cur_el = element
+    for id in id_parts:
+        if type(cur_el) == dict and id in cur_el.keys():
+            cur_el = cur_el[id]
+        elif type(cur_el) == dict and 'item_name' in cur_el.keys() and cur_el['item_name'] == id:
+            pass
+        elif type(cur_el) == list:
+            found = False
+            for cur_el_item in cur_el:
+                if cur_el_item['item_name'] == id and 'item_default' in cur_el_item.keys():
+                    cur_el = cur_el_item
+                    found = True
+            if not found:
+                raise DataNotFoundError(id + " in " + str(cur_el))
+        else:
+            raise DataNotFoundError(id + " in " + str(cur_el))
+    return cur_el
+
+def check_type(specification, value):
+    """Returns true if the value is of the correct type given the
+       specification"""
+    if type(specification) == list:
+        data_type = "list"
+    else:
+        data_type = specification['item_type']
+
+    if data_type == "integer" and type(value) != int:
+        raise DataTypeError(str(value) + " should be an integer")
+    elif data_type == "real" and type(value) != double:
+        raise DataTypeError(str(value) + " should be a real")
+    elif data_type == "boolean" and type(value) != boolean:
+        raise DataTypeError(str(value) + " should be a boolean")
+    elif data_type == "string" and type(value) != str:
+        raise DataTypeError(str(value) + " should be a string")
+    elif data_type == "list":
+        if type(value) != list:
+            raise DataTypeError(str(value) + " should be a list, not a " + str(value.__class__.__name__))
+        else:
+            # todo: check subtypes etc
+            for element in value:
+                check_type(specification['list_item_spec'], element)
+    elif data_type == "map" and type(value) != dict:
+        # todo: check subtypes etc
+        raise DataTypeError(str(value) + " should be a map")
+
+def spec_name_list(spec, prefix="", recurse=False):
+    """Returns a full list of all possible item identifiers in the
+       specification (part)"""
+    result = []
+    if prefix != "" and not prefix.endswith("/"):
+        prefix += "/"
+    if type(spec) == dict:
+        for name in spec:
+            result.append(prefix + name + "/")
+            if recurse:
+                result.extend(spec_name_list(spec[name],name, recurse))
+    elif type(spec) == list:
+        for list_el in spec:
+            if 'item_name' in list_el:
+                if list_el['item_type'] == dict:
+                    if recurse:
+                        result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
+                else:
+                    name = list_el['item_name']
+                    if list_el['item_type'] in ["list", "map"]:
+                        name += "/"
+                    result.append(name)
+
+    return result
+
+class ConfigData:
+    def __init__(self, specification):
+        self.specification = specification
+        self.data = {}
+
+    def get_item_list(self, identifier = None):
+        if identifier:
+            spec = find_spec(self.specification, identifier)
+            return spec_name_list(spec, identifier + "/")
+        return spec_name_list(self.specification)
+
+    def get_value(self, identifier):
+        """Returns a tuple where the first item is the value at the
+           given identifier, and the second item is a bool which is
+           true if the value is an unset default"""
+        value = find_no_exc(self.data, identifier)
+        if value:
+            return value, False
+        spec = find_spec(self.specification, identifier)
+        if spec and 'item_default' in spec:
+            return spec['item_default'], True
+        return None, False
+
+class UIConfigData():
+    def __init__(self, name, cc):
+        self.module_name = name
+        data_spec = self.get_data_specification(cc)
+        self.config = ConfigData(data_spec)
+        self.get_config_data(cc)
+        self.config_changes = {}
+    
+    def get_config_data(self, cc):
+        cc.group_sendmsg({ "command": ["get_config", self.module_name] }, "ConfigManager")
+        answer, env = cc.group_recvmsg(False)
+        if 'result' in answer.keys() and type(answer['result']) == list:
+            # TODO: with the new cc implementation, replace "1" by 1
+            if answer['result'][0] == "1":
+                # todo: exception
+                print("Error: " + str(answer['result'][1]))
+            else:
+                self.config.data = answer['result'][1]
+        else:
+            # XX todo: raise exc
+            print("Error: unexpected answer from config manager:")
+            print(answer)
+
+    def send_changes(self, cc):
+        """Sends the changes configuration values to the config manager.
+           If the command succeeds, the changes are re-requested and
+           the changed list is reset"""
+        cc.group_sendmsg({ "command": [ "set_config", self.module_name, self.config_changes ]}, "ConfigManager")
+        answer, env = cc.group_recvmsg(False)
+        if 'result' in answer and type(answer['result']) == list:
+            # TODO: with the new cc implementation, replace "0" by 0
+            if answer['result'][0] == "0":
+                # ok
+                self.get_config_data(cc)
+                self.config_changes = {}
+            else:
+                print("Error committing changes: " + answer['result'][1])
+        else:
+            print("Error: unexpected answer: " + str(answer))
+    
+    def get_data_specification(self, cc):
+        cc.group_sendmsg({ "command": ["get_data_spec", self.module_name] }, "ConfigManager")
+        answer, env = cc.group_recvmsg(False)
+        if 'result' in answer.keys() and type(answer['result']) == list:
+            # TODO: with the new cc implementation, replace "1" by 1
+            if answer['result'][0] == "1":
+                # todo: exception
+                print("Error: " + str(answer['result'][1]))
+                return None
+            else:
+                return answer['result'][1]
+        else:
+            # XX todo: raise exc
+            print("Error: unexpected answer from config manager:")
+            print(answer)
+        return None
+
+    def set(self, identifier, value):
+        # check against definition
+        spec = find_spec(identifier)
+        check_type(spec, value)
+        set(self.config_changes, identifier, value)
+
+    def get_value(self, identifier):
+        """Returns a three-tuple, where the first item is the value
+           (or None), the second is a boolean specifying whether
+           the value is the default value, and the third is a boolean
+           specifying whether the value is an uncommitted change"""
+        value = find_no_exc(self.config_changes, identifier)
+        if value:
+            return value, False, True
+        value, default = self.config.get_value(identifier)
+        if value:
+            return value, default, False
+        return None, False, False
+
+    def get_value_map(self, identifier, entry):
+        result_part = {}
+        result_part['name'] = entry['item_name']
+        result_part['type'] = entry['item_type']
+        value, default, modified = self.get_value(identifier + "/" + entry['item_name'])
+        # should we check type and only set int, double, bool and string here?
+        result_part['value'] = value
+        result_part['default'] = default
+        result_part['modified'] = modified
+        return result_part
+
+    def get_value_maps(self, identifier = None):
+        """Returns a list of maps, containing the following values:
+           name: name of the entry (string)
+           type: string containing the type of the value (or 'module')
+           value: value of the entry if it is a string, int, double or bool
+           modified: true if the value is a local change
+           default: true if the value has been changed
+           Throws DataNotFoundError if the identifier is bad
+        """
+        spec = find_spec(self.config.specification, identifier)
+        result = []
+        if type(spec) == dict:
+            # either the top-level list of modules or a spec map
+            if 'item_name' in spec:
+                result_part = self.get_value_map(identifier, spec)
+                if result_part['type'] == "list":
+                    values = self.get_value(identifier)[0]
+                    if values:
+                        for value in values:
+                            result_part2 = {}
+                            li_spec = spec['list_item_spec']
+                            result_part2['name'] = li_spec['item_name']
+                            result_part2['value'] = value
+                            result_part2['type'] = li_spec['item_type']
+                            result_part2['default'] = False
+                            result_part2['modified'] = False
+                            result.append(result_part2)
+                else:
+                    result.append(result_part)
+                
+            else:
+                for name in spec:
+                    result_part = {}
+                    result_part['name'] = name
+                    result_part['type'] = "module"
+                    result_part['value'] = None
+                    result_part['default'] = False
+                    result_part['modified'] = False
+                    result.append(result_part)
+        elif type(spec) == list:
+            for entry in spec:
+                if type(entry) == dict and 'item_name' in entry:
+                    result.append(self.get_value_map(identifier, entry))
+        return result
+
+    def add(self, identifier, value_str):
+        data_spec = find_spec(self.config.specification, identifier)
+        value = ast.literal_eval(value_str)
+        check_type(data_spec, [value])
+        cur_list = find_no_exc(self.config_changes, identifier)
+        if not cur_list:
+            cur_list = find_no_exc(self.config.data, identifier)
+        if not cur_list:
+            cur_list = []
+        if value not in cur_list:
+            cur_list.append(value)
+        set(self.config_changes, identifier, cur_list)
+
+    def remove(self, identifier, value_str):
+        data_spec = find_spec(self.config.specification, identifier)
+        value = ast.literal_eval(value_str)
+        check_type(data_spec, [value])
+        cur_list = find_no_exc(self.config_changes, identifier)
+        if not cur_list:
+            cur_list = find_no_exc(self.config.data, identifier)
+        if not cur_list:
+            cur_list = []
+        if value in cur_list:
+            cur_list.remove(value)
+        set(self.config_changes, identifier, cur_list)
+
+    def set(self, identifier, value_str):
+        data_spec = find_spec(self.config.specification, identifier)
+        value = ast.literal_eval(value_str)
+        check_type(data_spec, value)
+        set(self.config_changes, identifier, value)
+
+    def unset(self, identifier):
+        # todo: check whether the value is optional?
+        unset(self.config_changes, identifier)
+
+    def revert(self):
+        self.config_changes = {}
+
+    def commit(self, cc):
+        self.send_changes(cc)