diff --git a/src/bin/bind10/bind10.py.in b/src/bin/bind10/bind10.py.in index 95321d7b291bc75e7fcb7e335585c86abd74dd72..bda9c8647f86ea003ec38d7f10502ac8048be33a 100644 --- a/src/bin/bind10/bind10.py.in +++ b/src/bin/bind10/bind10.py.in @@ -116,7 +116,7 @@ class BoB: print("[XX] handling new config:") print(new_config) errors = [] - if self.ccs.get_config_spec().get_module_spec().validate(False, new_config, errors): + if self.ccs.get_module_spec().validate(False, new_config, errors): print("[XX] new config validated") self.ccs.set_config(new_config) answer = isc.config.ccsession.create_answer(0) @@ -209,7 +209,7 @@ class BoB: time.sleep(1) if self.verbose: print("[XX] starting ccsession") - self.ccs = isc.config.CCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler) + self.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler) self.ccs.start() if self.verbose: print("[XX] ccsession started") diff --git a/src/bin/bindctl/bindcmd.py b/src/bin/bindctl/bindcmd.py index ca1180cad1615a58a14a4c4f39472c7a9e70d76c..88b2984e3bda3f50330bd96d9efa295baf5cd206 100644 --- a/src/bin/bindctl/bindcmd.py +++ b/src/bin/bindctl/bindcmd.py @@ -86,7 +86,7 @@ class BindCmdInterpreter(Cmd): return False # Get all module information from cmd-ctrld - self.config_data = isc.config.UIConfigData(self) + self.config_data = isc.config.UIModuleCCSession(self) self.update_commands() self.cmdloop() except KeyboardInterrupt: diff --git a/src/lib/config/python/isc/config/ccsession.py b/src/lib/config/python/isc/config/ccsession.py index a39cc83ec44729451308963a019063cb0bcc4e08..a8d8aa5bad84a166fe88db0df72284928064946b 100644 --- a/src/lib/config/python/isc/config/ccsession.py +++ b/src/lib/config/python/isc/config/ccsession.py @@ -21,27 +21,28 @@ # modeled after ccsession.h/cc 'protocol' changes here need to be # made there as well -"""This module provides the CCSession class, as well as a set of +"""This module provides the ModuleCCSession class, as well as a set of utility functions to create and parse messages related to commands and configuration""" from isc.cc import Session +from isc.config.config_data import ConfigData, MultiConfigData import isc -class CCSessionError(Exception): pass +class ModuleCCSessionError(Exception): pass def parse_answer(msg): """Returns a tuple (rcode, value), where value depends on the command that was called. If rcode != 0, value is a string containing an error message""" if 'result' not in msg: - raise CCSessionError("answer message does not contain 'result' element") + raise ModuleCCSessionError("answer message does not contain 'result' element") elif type(msg['result']) != list: - raise CCSessionError("wrong result type in answer message") + raise ModuleCCSessionError("wrong result type in answer message") elif len(msg['result']) < 1: - raise CCSessionError("empty result list in answer message") + raise ModuleCCSessionError("empty result list in answer message") elif type(msg['result'][0]) != int: - raise CCSessionError("wrong rcode type in answer message") + raise ModuleCCSessionError("wrong rcode type in answer message") else: if len(msg['result']) > 1: return msg['result'][0], msg['result'][1] @@ -54,28 +55,28 @@ def create_answer(rcode, arg = None): on what the command or option was. If rcode != 0, arg must be a string containing an error message""" if type(rcode) != int: - raise CCSessionError("rcode in create_answer() must be an integer") + raise ModuleCCSessionError("rcode in create_answer() must be an integer") if rcode != 0 and type(arg) != str: - raise CCSessionError("arg in create_answer for rcode != 0 must be a string describing the error") + raise ModuleCCSessionError("arg in create_answer for rcode != 0 must be a string describing the error") if arg != None: return { 'result': [ rcode, arg ] } else: return { 'result': [ rcode ] } -class CCSession: +class ModuleCCSession: """This class maintains a connection to the command channel, as well as configuration options for modules. The module provides a specification file that contains the module name, configuration - options, and commands. It also gives the CCSession two callback + options, and commands. It also gives the ModuleCCSession two callback functions, one to call when there is a direct command to the module, and one to update the configuration run-time. These callbacks are called when 'check_command' is called on the - CCSession""" + ModuleCCSession""" def __init__(self, spec_file_name, config_handler, command_handler): - """Initialize a CCSession. This does *NOT* send the + """Initialize a ModuleCCSession. This does *NOT* send the specification and request the configuration yet. Use start() - for that once the CCSession has been initialized. + for that once the ModuleCCSession has been initialized. specfile_name is the path to the specification file config_handler and command_handler are callback functions, see set_config_handler and set_command_handler for more @@ -137,8 +138,6 @@ class CCSession: if msg: answer = None try: - print("[XX] got msg: ") - print(msg) if "config_update" in msg and self._config_handler: answer = self._config_handler(msg["config_update"]) if "command" in msg and self._command_handler: @@ -163,11 +162,8 @@ class CCSession: def __send_spec(self): """Sends the data specification to the configuration manager""" - print("[XX] send spec for " + self._module_name + " to ConfigManager") self._session.group_sendmsg({ "module_spec": self._config_data.get_module_spec().get_full_spec() }, "ConfigManager") answer, env = self._session.group_recvmsg(False) - print("[XX] got answer from cfgmgr:") - print(answer) def __request_config(self): """Asks the configuration manager for the current configuration, and call the config handler if set""" @@ -175,11 +171,75 @@ class CCSession: answer, env = self._session.group_recvmsg(False) rcode, value = parse_answer(answer) if rcode == 0: - if self._config_data.get_module_spec().validate(False, value): + if value != None and self._config_data.get_module_spec().validate(False, value): self._config_data.set_local_config(value); if self._config_handler: self._config_handler(value) else: # log error print("Error requesting configuration: " + value) - + +class UIModuleCCSession(MultiConfigData): + """This class is used in a configuration user interface. It contains + specific functions for getting, displaying, and sending + configuration settings.""" + def __init__(self, conn): + MultiConfigData.__init__(self) + self._conn = conn + self.request_specifications() + self.request_current_config() + + def request_specifications(self): + # this step should be unnecessary but is the current way cmdctl returns stuff + # so changes are needed there to make this clean (we need a command to simply get the + # full specs for everything, including commands etc, not separate gets for that) + specs = self._conn.send_GET('/config_spec') + commands = self._conn.send_GET('/commands') + for module in specs.keys(): + cur_spec = { 'module_name': module } + if module in specs and specs[module]: + cur_spec['config_data'] = specs[module] + if module in commands and commands[module]: + cur_spec['commands'] = commands[module] + + self.set_specification(isc.config.ModuleSpec(cur_spec)) + + def request_current_config(self): + config = self._conn.send_GET('/config_data') + if 'version' not in config or config['version'] != 1: + raise Exception("Bad config version") + self.set_local_config(config) + + def add_value(self, identifier, value_str): + module_spec = self.find_spec_part(identifier) + if (type(module_spec) != dict or "list_item_spec" not in module_spec): + raise DataTypeError(identifier + " is not a list") + value = isc.cc.data.parse_value_str(value_str) + cur_list, status = self.get_value(identifier) + if not cur_list: + cur_list = [] + if value not in cur_list: + cur_list.append(value) + self.set_value(identifier, cur_list) + + def remove_value(self, identifier, value_str): + module_spec = find_spec(self.config.specification, identifier) + if (type(module_spec) != dict or "list_item_spec" not in module_spec): + raise DataTypeError(identifier + " is not a list") + value = parse_value_str(value_str) + check_type(module_spec, [value]) + cur_list = isc.cc.data.find_no_exc(self.config_changes, identifier) + if not cur_list: + cur_list = isc.cc.data.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 commit(self): + if self.get_local_changes(): + self._conn.send_POST('/ConfigManager/set_config', self.get_local_changes()) + # todo: check result + self.request_current_config() + self.clear_local_changes() diff --git a/src/lib/config/python/isc/config/cfgmgr_test.py b/src/lib/config/python/isc/config/cfgmgr_test.py index af7e6f18e770301cbd02f56b95706965f96f08fb..733ca3287e611b54d1d8fbc79e50360b86625871 100644 --- a/src/lib/config/python/isc/config/cfgmgr_test.py +++ b/src/lib/config/python/isc/config/cfgmgr_test.py @@ -62,7 +62,7 @@ class TestConfigManagerData(unittest.TestCase): # # We can probably use a more general version of this # -class FakeCCSession: +class FakeModuleCCSession: def __init__(self): self.subscriptions = {} # each entry is of the form [ channel, instance, message ] @@ -106,7 +106,7 @@ class TestConfigManager(unittest.TestCase): def setUp(self): self.data_path = os.environ['CONFIG_TESTDATA_PATH'] - self.fake_session = FakeCCSession() + self.fake_session = FakeModuleCCSession() self.cm = ConfigManager(self.data_path, self.fake_session) self.name = "TestModule" self.spec = isc.config.module_spec_from_file(self.data_path + os.sep + "/spec2.spec") diff --git a/src/lib/config/python/isc/config/config_data.py b/src/lib/config/python/isc/config/config_data.py index bcc68e32cafd962afc875db9815211571f3e0876..871bb8f31d740093e0cc7844ea1fc0cceee078b2 100644 --- a/src/lib/config/python/isc/config/config_data.py +++ b/src/lib/config/python/isc/config/config_data.py @@ -14,9 +14,10 @@ # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -# Class to store configuration data and data definition -# Used by the config manager and python modules that communicate -# with the configuration manager +# Classes to store configuration data and data specifications +# +# Used by the config manager, (python) modules, and UI's (those last +# two through the classes in ccsession) # @@ -173,19 +174,23 @@ class MultiConfigData: self._local_changes = {} def set_specification(self, spec): + """Add or update a ModuleSpec""" if type(spec) != isc.config.ModuleSpec: raise Exception("not a datadef") self._specifications[spec.get_module_name()] = spec def get_module_spec(self, module): + """Returns the ModuleSpec for the module with the given name""" if module in self._specifications: return self._specifications[module] else: return None def find_spec_part(self, identifier): - """returns the specification for the item at the given - identifier, or None if not found""" + """Returns the specification for the item at the given + identifier, or None if not found. The first part of the + identifier (up to the first /) is interpreted as the module + name.""" if identifier[0] == '/': identifier = identifier[1:] module, sep, id = identifier.partition("/") @@ -194,30 +199,52 @@ class MultiConfigData: except isc.cc.data.DataNotFoundError as dnfe: return None - def set_current_config(self, config): + # this function should only be called by __request_config + def __set_current_config(self, config): + """Replace the full current config values.""" self._current_config = config def get_current_config(self): - """The current config is a dict where the first level is + """Returns the current configuration as it is known by the + configuration manager. It is a dict where the first level is the module name, and the value is the config values for that module""" return self._current_config def get_local_changes(self): + """Returns the local config changes, i.e. those that have not + been committed yet and are not known by the configuration + manager or the modules.""" return self._local_changes def clear_local_changes(self): + """Reverts all local changes""" self._local_changes = {} def get_local_value(self, identifier): + """Returns a specific local (uncommitted) configuration value, + as specified by the identifier. If the local changes do not + contain a new setting for this identifier, or if the + identifier cannot be found, None is returned. See + get_value() for a general way to find a configuration value + """ return isc.cc.data.find_no_exc(self._local_changes, identifier) def get_current_value(self, identifier): - """Returns the current non-default value, or None if not set""" + """Returns the current non-default value as known by the + configuration manager, or None if it is not set. + See get_value() for a general way to find a configuration + value + """ return isc.cc.data.find_no_exc(self._current_config, identifier) def get_default_value(self, identifier): - """returns the default value, or None if there is no default""" + """Returns the default value for the given identifier as + specified by the module specification, or None if there is + no default or the identifier could not be found. + See get_value() for a general way to find a configuration + value + """ if identifier[0] == '/': identifier = identifier[1:] module, sep, id = identifier.partition("/") @@ -231,10 +258,12 @@ class MultiConfigData: return None def get_value(self, identifier): - """Returns a tuple containing value,status. Status is either - LOCAL, CURRENT, DEFAULT or NONE, corresponding to the - source of the value (local change, current setting, default - as specified by the specification, or not found at all).""" + """Returns a tuple containing value,status. + The value contains the configuration value for the given + identifier. The status reports where this value came from; + it is one of: LOCAL, CURRENT, DEFAULT or NONE, corresponding + (local change, current setting, default as specified by the + specification, or not found at all).""" value = self.get_local_value(identifier) if value: return value, self.LOCAL @@ -253,8 +282,8 @@ class MultiConfigData: 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 TODO: use the consts for those last ones + Throws DataNotFoundError if the identifier is bad """ result = [] if not identifier: @@ -327,8 +356,8 @@ class MultiConfigData: def set_value(self, identifier, value): """Set the local value at the given identifier to value""" spec_part = self.find_spec_part(identifier) - if check_type(spec_part, value): - isc.cc.data.set(self._local_changes, identifier, value) + check_type(spec_part, value) + isc.cc.data.set(self._local_changes, identifier, value) def get_config_item_list(self, identifier = None): """Returns a list of strings containing the item_names of @@ -343,254 +372,3 @@ class MultiConfigData: return self._specifications.keys() -class UIConfigData(): - """This class is used in a configuration user interface. It contains - specific functions for getting, displaying, and sending - configuration settings.""" - def __init__(self, conn): - self._conn = conn - self._data = MultiConfigData() - self.request_specifications() - self.request_current_config() - a,b = self._data.get_value("/Boss/some_string") - - def request_specifications(self): - # this step should be unnecessary but is the current way cmdctl returns stuff - # so changes are needed there to make this clean (we need a command to simply get the - # full specs for everything, including commands etc, not separate gets for that) - specs = self._conn.send_GET('/config_spec') - commands = self._conn.send_GET('/commands') - #print(specs) - #print(commands) - for module in specs.keys(): - cur_spec = { 'module_name': module } - if module in specs and specs[module]: - cur_spec['config_data'] = specs[module] - if module in commands and commands[module]: - cur_spec['commands'] = commands[module] - - self._data.set_specification(isc.config.ModuleSpec(cur_spec)) - - def request_current_config(self): - config = self._conn.send_GET('/config_data') - if 'version' not in config or config['version'] != 1: - raise Exception("Bad config version") - self._data.set_current_config(config) - - def get_value(self, identifier): - return self._data.get_value(identifier) - - def set_value(self, identifier, value): - return self._data.set_value(identifier, value); - - def add_value(self, identifier, value_str): - module_spec = self._data.find_spec_part(identifier) - if (type(module_spec) != dict or "list_item_spec" not in module_spec): - raise DataTypeError(identifier + " is not a list") - value = isc.cc.data.parse_value_str(value_str) - cur_list, status = self.get_value(identifier) - if not cur_list: - cur_list = [] - if value not in cur_list: - cur_list.append(value) - self.set_value(identifier, cur_list) - - def remove_value(self, identifier, value_str): - module_spec = find_spec(self.config.specification, identifier) - if (type(module_spec) != dict or "list_item_spec" not in module_spec): - raise DataTypeError(identifier + " is not a list") - value = parse_value_str(value_str) - check_type(module_spec, [value]) - cur_list = isc.cc.data.find_no_exc(self.config_changes, identifier) - if not cur_list: - cur_list = isc.cc.data.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 get_value_maps(self, identifier = None): - return self._data.get_value_maps(identifier) - - def get_local_changes(self): - return self._data.get_local_changes() - - def commit(self): - self._conn.send_POST('/ConfigManager/set_config', self._data.get_local_changes()) - # todo: check result - self.request_current_config() - self._data.clear_local_changes() - - def get_config_item_list(self, identifier = None): - return self._data.get_config_item_list(identifier) - -# remove -class OUIConfigData(): - """This class is used in a configuration user interface. It contains - specific functions for getting, displaying, and sending - configuration settings.""" - def __init__(self, conn): - # the specs dict contains module: configdata elements - # these should all be replaced by the new stuff - module_spec = self.get_module_spec(conn) - self.config = module_spec - self.get_config_spec(conn) - self.config_changes = {} - # - self.config_ - self.specs = self.get_module_specs(conn) - - - def get_config_spec(self, conn): - data = conn.send_GET('/config_data') - - def send_changes(self, conn): - conn.send_POST('/ConfigManager/set_config', self.config_changes) - # Get latest config data - self.get_config_spec(conn) - self.config_changes = {} - - def get_module_spec(self, conn): - return conn.send_GET('/config_spec') - - def get_module_specs(self, conn): - specs = {} - allspecs = conn.send_GET('/config_spec') - - - 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 = isc.cc.data.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_single(self, identifier, entry): - """Returns a single entry for a value_map, where the value is - not a part of a bigger map""" - result_part = {} - result_part['name'] = entry['item_name'] - result_part['type'] = entry['item_type'] - value, default, modified = self.get_value(identifier) - # 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_map(self, identifier, entry): - """Returns a single entry for a value_map, where the value is - a part of a bigger map""" - 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, 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_single(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): - module_spec = find_spec(self.config.specification, identifier) - if (type(module_spec) != dict or "list_item_spec" not in module_spec): - raise DataTypeError(identifier + " is not a list") - value = parse_value_str(value_str) - check_type(module_spec, [value]) - cur_list = isc.cc.data.find_no_exc(self.config_changes, identifier) - if not cur_list: - cur_list = isc.cc.data.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): - module_spec = find_spec(self.config.specification, identifier) - if (type(module_spec) != dict or "list_item_spec" not in module_spec): - raise DataTypeError(identifier + " is not a list") - value = parse_value_str(value_str) - check_type(module_spec, [value]) - cur_list = isc.cc.data.find_no_exc(self.config_changes, identifier) - if not cur_list: - cur_list = isc.cc.data.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): - module_spec = find_spec(self.config.specification, identifier) - value = parse_value_str(value_str) - check_type(module_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, conn): - self.send_changes(conn) diff --git a/src/lib/config/python/isc/config/config_data_test.py b/src/lib/config/python/isc/config/config_data_test.py index fe27a7c10e14226c8d3296304046e196f73ed5da..0364babd082a2f83076d5df90bbd5735521ea7ca 100644 --- a/src/lib/config/python/isc/config/config_data_test.py +++ b/src/lib/config/python/isc/config/config_data_test.py @@ -14,7 +14,7 @@ # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # -# Tests for the ConfigData and UIConfigData classes +# Tests for the ConfigData and MultiConfigData classes # import unittest