config_data.py 15.8 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
    """Returns true if the value is of the correct type given the
Jelte Jansen's avatar
Jelte Jansen committed
30
       specification part relevant for the value"""
Jelte Jansen's avatar
Jelte Jansen committed
31
    if type(spec_part) == list:
32
33
        data_type = "list"
    else:
Jelte Jansen's avatar
Jelte Jansen committed
34
        data_type = spec_part['item_type']
35
36

    if data_type == "integer" and type(value) != int:
Jelte Jansen's avatar
Jelte Jansen committed
37
        raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
38
    elif data_type == "real" and type(value) != double:
Jelte Jansen's avatar
Jelte Jansen committed
39
        raise isc.cc.data.DataTypeError(str(value) + " is not a real")
40
    elif data_type == "boolean" and type(value) != boolean:
Jelte Jansen's avatar
Jelte Jansen committed
41
        raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
42
    elif data_type == "string" and type(value) != str:
Jelte Jansen's avatar
Jelte Jansen committed
43
        raise isc.cc.data.DataTypeError(str(value) + " is not a string")
44
45
    elif data_type == "list":
        if type(value) != list:
Jelte Jansen's avatar
Jelte Jansen committed
46
            raise isc.cc.data.DataTypeError(str(value) + " is not a list")
47
48
        else:
            for element in value:
Jelte Jansen's avatar
Jelte Jansen committed
49
                check_type(spec_part['list_item_spec'], element)
50
    elif data_type == "map" and type(value) != dict:
Jelte Jansen's avatar
Jelte Jansen committed
51
52
        # todo: check types of map contents too
        raise isc.cc.data.DataTypeError(str(value) + " is not a map")
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

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"""
    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
        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:
        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:
105
    """This class stores the module specs and the current non-default
106
107
108
109
110
       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
111
112
113
           of type ModuleSpec, a ConfigDataError is raised."""
        if type(specification) != isc.config.ModuleSpec:
            raise ConfigDataError("specification is of type " + str(type(specification)) + ", not ModuleSpec")
114
115
116
117
118
119
120
        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
           true if the value is an unset default"""
Jelte Jansen's avatar
Jelte Jansen committed
121
        value = isc.cc.data.find_no_exc(self.data, identifier)
122
123
        if value:
            return value, False
Jelte Jansen's avatar
Jelte Jansen committed
124
        spec = find_spec(self.specification.get_config_spec(), identifier)
125
126
127
128
        if spec and 'item_default' in spec:
            return spec['item_default'], True
        return None, False

Jelte Jansen's avatar
Jelte Jansen committed
129
130
    def get_module_spec(self):
        """Returns the ModuleSpec object associated with this ConfigData"""
131
132
133
134
135
136
137
138
        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
139
140
        return self.data;

Jelte Jansen's avatar
Jelte Jansen committed
141
142
143
144
145
146
147
148
149
    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:
            spec = find_spec(self.specification.get_config_spec(), identifier, recurse)
            return spec_name_list(spec, identifier + "/")
        return spec_name_list(self.specification.get_config_spec(), "", recurse)

Jelte Jansen's avatar
Jelte Jansen committed
150
    def get_full_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
151
152
153
154
155
156
        """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
157
        items = self.get_item_list(None, True)
Jelte Jansen's avatar
Jelte Jansen committed
158
        result = {}
Jelte Jansen's avatar
Jelte Jansen committed
159
160
        for item in items:
            value, default = self.get_value(item)
Jelte Jansen's avatar
Jelte Jansen committed
161
            result[item] = value
Jelte Jansen's avatar
Jelte Jansen committed
162
        return result
163

164
class MultiConfigData:
165
    """This class stores the module specs, current non-default
166
167
       configuration values and 'local' (uncommitted) changes for
       multiple modules"""
168
169
170
171
172
173
174
175
176
177
178
    LOCAL   = 1
    CURRENT = 2
    DEFAULT = 3
    NONE    = 4
    
    def __init__(self):
        self._specifications = {}
        self._current_config = {}
        self._local_changes = {}

    def set_specification(self, spec):
179
        """Add or update a ModuleSpec"""
Jelte Jansen's avatar
Jelte Jansen committed
180
        if type(spec) != isc.config.ModuleSpec:
181
182
183
            raise Exception("not a datadef")
        self._specifications[spec.get_module_name()] = spec

Jelte Jansen's avatar
Jelte Jansen committed
184
    def get_module_spec(self, module):
185
        """Returns the ModuleSpec for the module with the given name"""
186
187
188
189
190
191
        if module in self._specifications:
            return self._specifications[module]
        else:
            return None

    def find_spec_part(self, identifier):
192
193
194
195
        """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."""
196
197
198
199
        if identifier[0] == '/':
            identifier = identifier[1:]
        module, sep, id = identifier.partition("/")
        try:
Jelte Jansen's avatar
Jelte Jansen committed
200
            return find_spec(self._specifications[module].get_config_spec(), id)
201
202
203
        except isc.cc.data.DataNotFoundError as dnfe:
            return None

204
    # this function should only be called by __request_config
205
    def _set_current_config(self, config):
206
        """Replace the full current config values."""
207
208
209
        self._current_config = config

    def get_current_config(self):
210
211
        """Returns the current configuration as it is known by the
           configuration manager. It is a dict where the first level is
212
213
214
215
216
           the module name, and the value is the config values for
           that module"""
        return self._current_config
        
    def get_local_changes(self):
217
218
219
        """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."""
220
221
222
        return self._local_changes

    def clear_local_changes(self):
223
        """Reverts all local changes"""
224
225
226
        self._local_changes = {}

    def get_local_value(self, identifier):
227
228
229
230
231
232
        """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
           """
233
234
235
        return isc.cc.data.find_no_exc(self._local_changes, identifier)
        
    def get_current_value(self, identifier):
236
237
238
239
240
        """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
        """
241
242
243
        return isc.cc.data.find_no_exc(self._current_config, identifier)
        
    def get_default_value(self, identifier):
244
245
246
247
248
249
        """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
        """
250
251
252
253
        if identifier[0] == '/':
            identifier = identifier[1:]
        module, sep, id = identifier.partition("/")
        try:
Jelte Jansen's avatar
Jelte Jansen committed
254
            spec = find_spec(self._specifications[module].get_config_spec(), id)
255
256
257
258
259
260
261
262
            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):
263
264
265
266
267
268
        """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)."""
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
        value = self.get_local_value(identifier)
        if value:
            return value, self.LOCAL
        value = self.get_current_value(identifier)
        if value:
            return value, self.CURRENT
        value = self.get_default_value(identifier)
        if value:
            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
288
           Throws DataNotFoundError if the identifier is bad
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
        """
        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
305
            spec = self.get_module_spec(module)
306
            if spec:
Jelte Jansen's avatar
Jelte Jansen committed
307
                spec_part = find_spec(spec.get_config_spec(), id)
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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
                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):
        """Set the local value at the given identifier to value"""
Jelte Jansen's avatar
Jelte Jansen committed
360
        spec_part = self.find_spec_part(identifier)
361
362
        check_type(spec_part, value)
        isc.cc.data.set(self._local_changes, identifier, value)
363
 
364
365
366
367
    def get_config_item_list(self, identifier = None):
        """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
368
369
           the identifier (up to the first /) is interpreted as the
           module name"""
370
371
372
373
374
        if identifier:
            spec = self.find_spec_part(identifier)
            return spec_name_list(spec, identifier + "/")
        else:
            return self._specifications.keys()