config_data.py 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# 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.

Jelte Jansen's avatar
Jelte Jansen committed
16 17
"""
Classes to store configuration data and module specifications
18

Jelte Jansen's avatar
Jelte Jansen committed
19 20 21
Used by the config manager, (python) modules, and UI's (those last
two through the classes in ccsession)
"""
22 23

import isc.cc.data
24
import isc.config.module_spec
25 26 27

class ConfigDataError(Exception): pass

Jelte Jansen's avatar
Jelte Jansen committed
28
def check_type(spec_part, value):
29 30 31 32
    """Does nothing if the value is of the correct type given the
       specification part relevant for the value. Raises an
       isc.cc.data.DataTypeError exception if not. spec_part can be
       retrieved with find_spec_part()"""
Jelte Jansen's avatar
Jelte Jansen committed
33
    if type(spec_part) == list:
34 35
        data_type = "list"
    else:
Jelte Jansen's avatar
Jelte Jansen committed
36
        data_type = spec_part['item_type']
37 38

    if data_type == "integer" and type(value) != int:
Jelte Jansen's avatar
Jelte Jansen committed
39
        raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
40
    elif data_type == "real" and type(value) != float:
Jelte Jansen's avatar
Jelte Jansen committed
41
        raise isc.cc.data.DataTypeError(str(value) + " is not a real")
42
    elif data_type == "boolean" and type(value) != bool:
Jelte Jansen's avatar
Jelte Jansen committed
43
        raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
44
    elif data_type == "string" and type(value) != str:
Jelte Jansen's avatar
Jelte Jansen committed
45
        raise isc.cc.data.DataTypeError(str(value) + " is not a string")
46 47
    elif data_type == "list":
        if type(value) != list:
Jelte Jansen's avatar
Jelte Jansen committed
48
            raise isc.cc.data.DataTypeError(str(value) + " is not a list")
49 50
        else:
            for element in value:
Jelte Jansen's avatar
Jelte Jansen committed
51
                check_type(spec_part['list_item_spec'], element)
52
    elif data_type == "map" and type(value) != dict:
Jelte Jansen's avatar
Jelte Jansen committed
53 54
        # todo: check types of map contents too
        raise isc.cc.data.DataTypeError(str(value) + " is not a map")
55

56
def find_spec_part(element, identifier):
57 58 59 60 61 62 63 64 65 66 67 68
    """find the data definition for the given identifier
       returns either a map with 'item_name' etc, or a list of those"""
    if identifier == "":
        return element
    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
69 70 71 72 73 74 75 76
        elif type(cur_el) == dict and 'map_item_spec' in cur_el.keys():
            found = False
            for cur_el_item in cur_el['map_item_spec']:
                if cur_el_item['item_name'] == id:
                    cur_el = cur_el_item
                    found = True
            if not found:
                raise isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
        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 isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
        else:
            raise isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
    return cur_el

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:
96 97 98 99 100 101 102 103 104 105 106 107
        if 'map_item_spec' in spec:
            for map_el in spec['map_item_spec']:
                name = map_el['item_name']
                if map_el['item_type'] == 'map':
                    name += "/"
                result.append(prefix + name)
        else:
            for name in spec:
                result.append(prefix + name + "/")
                if recurse:
                    print("[XX] recurse1")
                    result.extend(spec_name_list(spec[name],name, recurse))
108 109 110
    elif type(spec) == list:
        for list_el in spec:
            if 'item_name' in list_el:
111 112
                if list_el['item_type'] == "map" and recurse:
                    result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
113 114 115 116
                else:
                    name = list_el['item_name']
                    if list_el['item_type'] in ["list", "map"]:
                        name += "/"
117
                    result.append(prefix + name)
118 119 120
    return result

class ConfigData:
121
    """This class stores the module specs and the current non-default
122 123 124 125 126
       config values. It provides functions to get the actual value or
       the default value if no non-default value has been set"""
   
    def __init__(self, specification):
        """Initialize a ConfigData instance. If specification is not
Jelte Jansen's avatar
Jelte Jansen committed
127 128 129
           of type ModuleSpec, a ConfigDataError is raised."""
        if type(specification) != isc.config.ModuleSpec:
            raise ConfigDataError("specification is of type " + str(type(specification)) + ", not ModuleSpec")
130 131 132 133 134 135
        self.specification = specification
        self.data = {}

    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
136 137
           true if the value is an unset default. Raises an
           isc.cc.data.DataNotFoundError if the identifier is bad"""
Jelte Jansen's avatar
Jelte Jansen committed
138
        value = isc.cc.data.find_no_exc(self.data, identifier)
139
        if value != None:
140
            return value, False
141
        spec = find_spec_part(self.specification.get_config_spec(), identifier)
142 143 144 145
        if spec and 'item_default' in spec:
            return spec['item_default'], True
        return None, False

Jelte Jansen's avatar
Jelte Jansen committed
146 147
    def get_module_spec(self):
        """Returns the ModuleSpec object associated with this ConfigData"""
148 149 150 151 152 153 154 155
        return self.specification

    def set_local_config(self, data):
        """Set the non-default config values, as passed by cfgmgr"""
        self.data = data

    def get_local_config(self):
        """Returns the non-default config values in a dict"""
Jelte Jansen's avatar
Jelte Jansen committed
156 157
        return self.data;

Jelte Jansen's avatar
Jelte Jansen committed
158 159 160 161 162
    def get_item_list(self, identifier = None, recurse = False):
        """Returns a list of strings containing the full identifiers of
           all 'sub'options at the given identifier. If recurse is True,
           it will also add all identifiers of all children, if any"""
        if identifier:
163
            spec = find_spec_part(self.specification.get_config_spec(), identifier)
Jelte Jansen's avatar
Jelte Jansen committed
164 165 166
            return spec_name_list(spec, identifier + "/")
        return spec_name_list(self.specification.get_config_spec(), "", recurse)

Jelte Jansen's avatar
Jelte Jansen committed
167
    def get_full_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
168 169 170 171 172 173
        """Returns a dict containing identifier: value elements, for
           all configuration options for this module. If there is
           a local setting, that will be used. Otherwise the value
           will be the default as specified by the module specification.
           If there is no default and no local setting, the value will
           be None"""
Jelte Jansen's avatar
Jelte Jansen committed
174
        items = self.get_item_list(None, True)
Jelte Jansen's avatar
Jelte Jansen committed
175
        result = {}
Jelte Jansen's avatar
Jelte Jansen committed
176 177
        for item in items:
            value, default = self.get_value(item)
Jelte Jansen's avatar
Jelte Jansen committed
178
            result[item] = value
Jelte Jansen's avatar
Jelte Jansen committed
179
        return result
180

181
class MultiConfigData:
182
    """This class stores the module specs, current non-default
183 184
       configuration values and 'local' (uncommitted) changes for
       multiple modules"""
185 186 187 188 189 190 191 192 193 194 195
    LOCAL   = 1
    CURRENT = 2
    DEFAULT = 3
    NONE    = 4
    
    def __init__(self):
        self._specifications = {}
        self._current_config = {}
        self._local_changes = {}

    def set_specification(self, spec):
196
        """Add or update a ModuleSpec"""
Jelte Jansen's avatar
Jelte Jansen committed
197
        if type(spec) != isc.config.ModuleSpec:
198 199 200
            raise Exception("not a datadef")
        self._specifications[spec.get_module_name()] = spec

Jelte Jansen's avatar
Jelte Jansen committed
201
    def get_module_spec(self, module):
202 203
        """Returns the ModuleSpec for the module with the given name.
           If there is no such module, it returns None"""
204 205 206 207 208 209
        if module in self._specifications:
            return self._specifications[module]
        else:
            return None

    def find_spec_part(self, identifier):
210 211 212
        """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
213
           name. Returns None if not found."""
214 215 216 217
        if identifier[0] == '/':
            identifier = identifier[1:]
        module, sep, id = identifier.partition("/")
        try:
218
            return find_spec_part(self._specifications[module].get_config_spec(), id)
219 220
        except isc.cc.data.DataNotFoundError as dnfe:
            return None
221 222
        except KeyError as ke:
            return None
223

224
    # this function should only be called by __request_config
225
    def _set_current_config(self, config):
226
        """Replace the full current config values."""
227 228 229
        self._current_config = config

    def get_current_config(self):
230 231
        """Returns the current configuration as it is known by the
           configuration manager. It is a dict where the first level is
232 233 234 235 236
           the module name, and the value is the config values for
           that module"""
        return self._current_config
        
    def get_local_changes(self):
237 238 239
        """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."""
240 241 242
        return self._local_changes

    def clear_local_changes(self):
243
        """Reverts all local changes"""
244 245 246
        self._local_changes = {}

    def get_local_value(self, identifier):
247 248 249 250 251 252
        """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
           """
253 254 255
        return isc.cc.data.find_no_exc(self._local_changes, identifier)
        
    def get_current_value(self, identifier):
256 257 258 259 260
        """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
        """
261 262 263
        return isc.cc.data.find_no_exc(self._current_config, identifier)
        
    def get_default_value(self, identifier):
264 265 266 267 268 269
        """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
        """
270 271 272 273
        if identifier[0] == '/':
            identifier = identifier[1:]
        module, sep, id = identifier.partition("/")
        try:
274
            spec = find_spec_part(self._specifications[module].get_config_spec(), id)
275 276 277 278 279 280 281 282
            if 'item_default' in spec:
                return spec['item_default']
            else:
                return None
        except isc.cc.data.DataNotFoundError as dnfe:
            return None

    def get_value(self, identifier):
283 284 285 286 287 288
        """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)."""
289
        value = self.get_local_value(identifier)
290
        if value != None:
291 292
            return value, self.LOCAL
        value = self.get_current_value(identifier)
293
        if value != None:
294 295
            return value, self.CURRENT
        value = self.get_default_value(identifier)
296
        if value != None:
297 298 299 300 301 302 303 304 305 306 307
            return value, self.DEFAULT
        return None, self.NONE

    def get_value_maps(self, identifier = None):
        """Returns a list of dicts, 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
           TODO: use the consts for those last ones
308
           Throws DataNotFoundError if the identifier is bad
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
        """
        result = []
        if not identifier:
            # No identifier, so we need the list of current modules
            for module in self._specifications.keys():
                entry = {}
                entry['name'] = module
                entry['type'] = 'module'
                entry['value'] = None
                entry['modified'] = False
                entry['default'] = False
                result.append(entry)
        else:
            if identifier[0] == '/':
                identifier = identifier[1:]
            module, sep, id = identifier.partition('/')
Jelte Jansen's avatar
Jelte Jansen committed
325
            spec = self.get_module_spec(module)
326
            if spec:
327
                spec_part = find_spec_part(spec.get_config_spec(), id)
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
                print(spec_part)
                if type(spec_part) == list:
                    for item in spec_part:
                        entry = {}
                        entry['name'] = item['item_name']
                        entry['type'] = item['item_type']
                        value, status = self.get_value("/" + identifier + "/" + item['item_name'])
                        entry['value'] = value
                        if status == self.LOCAL:
                            entry['modified'] = True
                        else:
                            entry['modified'] = False
                        if status == self.DEFAULT:
                            entry['default'] = False
                        else:
                            entry['default'] = False
                        result.append(entry)
                else:
                    item = spec_part
                    if item['item_type'] == 'list':
                        li_spec = item['list_item_spec']
                        l, status =  self.get_value("/" + identifier)
                        if l:
                            for value in l:
                                result_part2 = {}
                                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:
                        entry = {}
                        entry['name'] = item['item_name']
                        entry['type'] = item['item_type']
                        #value, status = self.get_value("/" + identifier + "/" + item['item_name'])
                        value, status = self.get_value("/" + identifier)
                        entry['value'] = value
                        if status == self.LOCAL:
                            entry['modified'] = True
                        else:
                            entry['modified'] = False
                        if status == self.DEFAULT:
                            entry['default'] = False
                        else:
                            entry['default'] = False
                        result.append(entry)
            #print(spec)
        return result

    def set_value(self, identifier, value):
379 380 381
        """Set the local value at the given identifier to value. If
           there is a specification for the given identifier, the type
           is checked."""
Jelte Jansen's avatar
Jelte Jansen committed
382
        spec_part = self.find_spec_part(identifier)
383 384
        if spec_part != None:
            check_type(spec_part, value)
385
        isc.cc.data.set(self._local_changes, identifier, value)
386
 
387
    def get_config_item_list(self, identifier = None, recurse = False):
388 389 390
        """Returns a list of strings containing the item_names of
           the child items at the given identifier. If no identifier is
           specified, returns a list of module names. The first part of
391 392
           the identifier (up to the first /) is interpreted as the
           module name"""
393 394
        if identifier:
            spec = self.find_spec_part(identifier)
395
            return spec_name_list(spec, identifier + "/", recurse)
396
        else:
397 398 399 400 401 402 403
            if recurse:
                id_list = []
                for module in self._specifications:
                    id_list.extend(spec_name_list(self._specifications[module], module, recurse))
                return id_list
            else:
                return list(self._specifications.keys())