config_data.py 36 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
Jelte Jansen's avatar
Jelte Jansen committed
25
import ast
26
27
28

class ConfigDataError(Exception): pass

29
30
BIND10_CONFIG_DATA_VERSION = 2

Jelte Jansen's avatar
Jelte Jansen committed
31
# Helper functions
Jelte Jansen's avatar
Jelte Jansen committed
32
def spec_part_is_list(spec_part):
Jelte Jansen's avatar
Jelte Jansen committed
33
34
    """Returns True if the given spec_part is a dict that contains a
       list specification, and False otherwise."""
Jelte Jansen's avatar
Jelte Jansen committed
35
36
37
    return (type(spec_part) == dict and 'list_item_spec' in spec_part)

def spec_part_is_map(spec_part):
Jelte Jansen's avatar
Jelte Jansen committed
38
39
    """Returns True if the given spec_part is a dict that contains a
       map specification, and False otherwise."""
Jelte Jansen's avatar
Jelte Jansen committed
40
41
42
    return (type(spec_part) == dict and 'map_item_spec' in spec_part)

def spec_part_is_named_set(spec_part):
Jelte Jansen's avatar
Jelte Jansen committed
43
44
    """Returns True if the given spec_part is a dict that contains a
       named_set specification, and False otherwise."""
45
    return (type(spec_part) == dict and 'named_set_item_spec' in spec_part)
Jelte Jansen's avatar
Jelte Jansen committed
46

Jelte Jansen's avatar
Jelte Jansen committed
47
def check_type(spec_part, value):
48
49
50
51
    """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()"""
52
    if type(spec_part) == dict and 'item_type' in spec_part:
Jelte Jansen's avatar
Jelte Jansen committed
53
        data_type = spec_part['item_type']
54
55
    else:
        raise isc.cc.data.DataTypeError(str("Incorrect specification part for type checking"))
56
57

    if data_type == "integer" and type(value) != int:
Jelte Jansen's avatar
Jelte Jansen committed
58
        raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
59
    elif data_type == "real" and type(value) != float:
Jelte Jansen's avatar
Jelte Jansen committed
60
        raise isc.cc.data.DataTypeError(str(value) + " is not a real")
61
    elif data_type == "boolean" and type(value) != bool:
Jelte Jansen's avatar
Jelte Jansen committed
62
        raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
63
    elif data_type == "string" and type(value) != str:
Jelte Jansen's avatar
Jelte Jansen committed
64
        raise isc.cc.data.DataTypeError(str(value) + " is not a string")
65
66
    elif data_type == "list":
        if type(value) != list:
Jelte Jansen's avatar
Jelte Jansen committed
67
            raise isc.cc.data.DataTypeError(str(value) + " is not a list")
68
69
        else:
            for element in value:
Jelte Jansen's avatar
Jelte Jansen committed
70
                check_type(spec_part['list_item_spec'], element)
71
    elif data_type == "map" and type(value) != dict:
Jelte Jansen's avatar
Jelte Jansen committed
72
73
        # todo: check types of map contents too
        raise isc.cc.data.DataTypeError(str(value) + " is not a map")
74

75
def convert_type(spec_part, value):
Jelte Jansen's avatar
Jelte Jansen committed
76
    """Convert the given value(type is string) according specification 
77
78
79
80
81
82
    part relevant for the value. Raises an isc.cc.data.DataTypeError 
    exception if conversion failed.
    """
    if type(spec_part) == dict and 'item_type' in spec_part:
        data_type = spec_part['item_type']
    else:
Jelte Jansen's avatar
Jelte Jansen committed
83
        raise isc.cc.data.DataTypeError(str("Incorrect specification part for type conversion"))
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
   
    try:
        if data_type == "integer":
            return int(value)
        elif data_type == "real":
            return float(value)
        elif data_type == "boolean":
            return str.lower(str(value)) != 'false'
        elif data_type == "string":
            return str(value)
        elif data_type == "list":
            ret = []
            if type(value) == list:
                for item in value:    
                    ret.append(convert_type(spec_part['list_item_spec'], item))
            elif type(value) == str:    
                value = value.split(',')
Jelte Jansen's avatar
Jelte Jansen committed
101
                for item in value:
102
103
                    sub_value = item.split()
                    for sub_item in sub_value:
Jelte Jansen's avatar
Jelte Jansen committed
104
105
                        ret.append(convert_type(spec_part['list_item_spec'],
                                                sub_item))
106
107
108
109
110
111

            if ret == []:
                raise isc.cc.data.DataTypeError(str(value) + " is not a list")

            return ret
        elif data_type == "map":
112
113
114
115
116
117
118
119
120
121
122
            try:
                map = ast.literal_eval(value)
                if type(map) == dict:
                    # todo: check types of map contents too
                    return map
                else:
                    raise isc.cc.data.DataTypeError(
                               "Value in convert_type not a string "
                               "specifying a dict")
            except SyntaxError as se:
                raise isc.cc.data.DataTypeError("Error parsing map: " + str(se))
123
124
125
126
127
128
129
        else:
            return value
    except ValueError as err:
        raise isc.cc.data.DataTypeError(str(err))
    except TypeError as err:
        raise isc.cc.data.DataTypeError(str(err))

130
131
132
133
def _get_map_or_list(spec_part):
    """Returns the list or map specification if this is a list or a
       map specification part. If not, returns the given spec_part
       itself"""
134
    if spec_part_is_map(spec_part):
135
        return spec_part["map_item_spec"]
136
    elif spec_part_is_list(spec_part):
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
        return spec_part["list_item_spec"]
    else:
        return spec_part

def _find_spec_part_single(cur_spec, id_part):
    """Find the spec part for the given (partial) name. This partial
       name does not contain separators ('/'), and the specification
       part should be a direct child of the given specification part.
       id_part may contain list selectors, which will be ignored.
       Returns the child part.
       Raises DataNotFoundError if it was not found."""
    # strip list selector part
    # don't need it for the spec part, so just drop it
    id, list_indices = isc.cc.data.split_identifier_list_indices(id_part)

    # The specification we want a sub-part for should be either a
    # list or a map, which is internally represented by a dict with
    # an element 'map_item_spec', a dict with an element 'list_item_spec',
155
    # or a list (when it is the 'main' config_data element of a module).
Jelte Jansen's avatar
Jelte Jansen committed
156
    if spec_part_is_map(cur_spec):
157
158
159
160
161
        for cur_spec_item in cur_spec['map_item_spec']:
            if cur_spec_item['item_name'] == id:
                return cur_spec_item
        # not found
        raise isc.cc.data.DataNotFoundError(id + " not found")
Jelte Jansen's avatar
Jelte Jansen committed
162
    elif spec_part_is_list(cur_spec):
163
164
165
166
        if cur_spec['item_name'] == id:
            return cur_spec['list_item_spec']
        # not found
        raise isc.cc.data.DataNotFoundError(id + " not found")
167
168
    elif type(cur_spec) == dict and 'named_set_item_spec' in cur_spec.keys():
        return cur_spec['named_set_item_spec']
169
170
171
172
173
174
175
176
177
    elif type(cur_spec) == list:
        for cur_spec_item in cur_spec:
            if cur_spec_item['item_name'] == id:
                return cur_spec_item
        # not found
        raise isc.cc.data.DataNotFoundError(id + " not found")
    else:
        raise isc.cc.data.DataNotFoundError("Not a correct config specification")

Jelte Jansen's avatar
Jelte Jansen committed
178
def find_spec_part(element, identifier, strict_identifier = True):
179
    """find the data definition for the given identifier
Jelte Jansen's avatar
Jelte Jansen committed
180
181
182
183
       returns either a map with 'item_name' etc, or a list of those
       Parameters:
       element: The specification element to start the search in
       identifier: The element to find (relative to element above)
184
185
186
187
       strict_identifier: If True (the default), additional checking occurs.
                          Currently the only check is whether a list index is
                          specified (except for the last part of the
                          identifier)
Jelte Jansen's avatar
Jelte Jansen committed
188
189
190
191
192
193
       Raises a DataNotFoundError if the data is not found, or if
       strict_identifier is True and any non-final identifier parts
       (i.e. before the last /) identify a list element and do not contain
       an index.
       Returns the spec element identified by the given identifier.
    """
194
195
196
197
198
    if identifier == "":
        return element
    id_parts = identifier.split("/")
    id_parts[:] = (value for value in id_parts if value != "")
    cur_el = element
Jelte Jansen's avatar
Jelte Jansen committed
199

200
201
202
203
204
205
    # up to the last element, if the result is a map or a list,
    # we want its subspecification (i.e. list_item_spec or
    # map_item_spec). For the last element in the identifier we
    # always want the 'full' spec of the item
    for id_part in id_parts[:-1]:
        cur_el = _find_spec_part_single(cur_el, id_part)
Jelte Jansen's avatar
Jelte Jansen committed
206
207
208
        if strict_identifier and spec_part_is_list(cur_el) and\
           not isc.cc.data.identifier_has_list_index(id_part):
            raise isc.cc.data.DataNotFoundError(id_part +
Jelte Jansen's avatar
Jelte Jansen committed
209
                                                " is a list and needs an index")
210
211
212
        cur_el = _get_map_or_list(cur_el)

    cur_el = _find_spec_part_single(cur_el, id_parts[-1])
213
214
215
216
    return cur_el

def spec_name_list(spec, prefix="", recurse=False):
    """Returns a full list of all possible item identifiers in the
217
218
       specification (part). Raises a ConfigDataError if spec is not
       a correct spec (as returned by ModuleSpec.get_config_spec()"""
219
220
221
222
    result = []
    if prefix != "" and not prefix.endswith("/"):
        prefix += "/"
    if type(spec) == dict:
223
        if spec_part_is_map(spec):
224
225
226
227
            for map_el in spec['map_item_spec']:
                name = map_el['item_name']
                if map_el['item_type'] == 'map':
                    name += "/"
228
                if recurse and spec_part_is_map(map_el):
229
230
231
                    result.extend(spec_name_list(map_el['map_item_spec'], prefix + map_el['item_name'], recurse))
                else:
                    result.append(prefix + name)
232
        elif 'named_set_item_spec' in spec:
233
234
            # we added a '/' above, but in this one case we don't want it
            result.append(prefix[:-1])
235
236
237
238
        else:
            for name in spec:
                result.append(prefix + name + "/")
                if recurse:
239
                    result.extend(spec_name_list(spec[name], name, recurse))
240
241
242
    elif type(spec) == list:
        for list_el in spec:
            if 'item_name' in list_el:
243
244
                if list_el['item_type'] == "map" and recurse:
                    result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
245
246
                else:
                    name = list_el['item_name']
247
                    result.append(prefix + name)
248
            else:
249
                raise ConfigDataError("Bad specification")
250
    else:
251
        raise ConfigDataError("Bad specification")
252
253
254
    return result

class ConfigData:
255
    """This class stores the module specs and the current non-default
256
257
258
259
260
       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
261
262
263
           of type ModuleSpec, a ConfigDataError is raised."""
        if type(specification) != isc.config.ModuleSpec:
            raise ConfigDataError("specification is of type " + str(type(specification)) + ", not ModuleSpec")
264
265
266
267
268
269
        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
270
271
           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
272
        value = isc.cc.data.find_no_exc(self.data, identifier)
273
        if value != None:
274
            return value, False
275
        spec = find_spec_part(self.specification.get_config_spec(), identifier)
276
277
278
279
        if spec and 'item_default' in spec:
            return spec['item_default'], True
        return None, False

280
281
282
    def get_default_value(self, identifier):
        """Returns the default from the specification, or None if there
           is no default"""
Jelte Jansen's avatar
Jelte Jansen committed
283
284
285
286
287
288
        # We are searching for the default value, so we can set
        # strict_identifier to false (in fact, we need to; we may not know
        # some list indices, or they may not exist, we are looking for
        # a default value for a reason here).
        spec = find_spec_part(self.specification.get_config_spec(),
                              identifier, False)
289
290
291
292
293
        if spec and 'item_default' in spec:
            return spec['item_default']
        else:
            return None

Jelte Jansen's avatar
Jelte Jansen committed
294
295
    def get_module_spec(self):
        """Returns the ModuleSpec object associated with this ConfigData"""
296
297
298
299
300
301
302
303
        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"""
304
        return self.data
Jelte Jansen's avatar
Jelte Jansen committed
305

Jelte Jansen's avatar
Jelte Jansen committed
306
307
308
309
310
    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:
311
            spec = find_spec_part(self.specification.get_config_spec(), identifier)
Jelte Jansen's avatar
Jelte Jansen committed
312
313
314
            return spec_name_list(spec, identifier + "/")
        return spec_name_list(self.specification.get_config_spec(), "", recurse)

Jelte Jansen's avatar
Jelte Jansen committed
315
    def get_full_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
316
317
318
319
320
321
        """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
322
        items = self.get_item_list(None, True)
Jelte Jansen's avatar
Jelte Jansen committed
323
        result = {}
Jelte Jansen's avatar
Jelte Jansen committed
324
325
        for item in items:
            value, default = self.get_value(item)
Jelte Jansen's avatar
Jelte Jansen committed
326
            result[item] = value
Jelte Jansen's avatar
Jelte Jansen committed
327
        return result
328

329
330
331
332
333
334
335
336
337
338
339
340
341
342
# should we just make a class for these?
def _create_value_map_entry(name, type, value, status = None):
    entry = {}
    entry['name'] = name
    entry['type'] = type
    entry['value'] = value
    entry['modified'] = False
    entry['default'] = False
    if status == MultiConfigData.LOCAL:
        entry['modified'] = True
    if status == MultiConfigData.DEFAULT:
        entry['default'] = True
    return entry

343
class MultiConfigData:
344
    """This class stores the module specs, current non-default
345
346
       configuration values and 'local' (uncommitted) changes for
       multiple modules"""
347
348
349
350
351
352
353
354
355
356
    LOCAL   = 1
    CURRENT = 2
    DEFAULT = 3
    NONE    = 4
    
    def __init__(self):
        self._specifications = {}
        self._current_config = {}
        self._local_changes = {}

357
358
359
360
    def clear_specifications(self):
        """Remove all known module specifications"""
        self._specifications = {}

361
    def set_specification(self, spec):
362
        """Add or update a ModuleSpec. Raises a ConfigDataError is spec is not a ModuleSpec"""
Jelte Jansen's avatar
Jelte Jansen committed
363
        if type(spec) != isc.config.ModuleSpec:
364
            raise ConfigDataError("not a datadef: " + str(type(spec)))
365
366
        self._specifications[spec.get_module_name()] = spec

367
368
369
370
371
    def remove_specification(self, module_name):
        """Removes the specification with the given module name. Does nothing if it wasn't there."""
        if module_name in self._specifications:
            del self._specifications[module_name]

372
373
374
375
376
    def have_specification(self, module_name):
        """Returns True if we have a specification for the module with the given name.
           Returns False if we do not."""
        return module_name in self._specifications

Jelte Jansen's avatar
Jelte Jansen committed
377
    def get_module_spec(self, module):
378
379
        """Returns the ModuleSpec for the module with the given name.
           If there is no such module, it returns None"""
380
381
382
383
384
385
        if module in self._specifications:
            return self._specifications[module]
        else:
            return None

    def find_spec_part(self, identifier):
386
387
388
        """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
389
390
           name. Returns None if not found, or if identifier is not a
           string."""
391
        if type(identifier) != str or identifier == "":
392
            return None
393
394
395
396
        if identifier[0] == '/':
            identifier = identifier[1:]
        module, sep, id = identifier.partition("/")
        try:
397
            return find_spec_part(self._specifications[module].get_config_spec(), id)
398
399
        except isc.cc.data.DataNotFoundError as dnfe:
            return None
400
401
        except KeyError as ke:
            return None
402

403
    # this function should only be called by __request_config
404
    def _set_current_config(self, config):
405
        """Replace the full current config values."""
406
407
408
        self._current_config = config

    def get_current_config(self):
409
410
        """Returns the current configuration as it is known by the
           configuration manager. It is a dict where the first level is
411
412
413
414
415
           the module name, and the value is the config values for
           that module"""
        return self._current_config
        
    def get_local_changes(self):
416
417
418
        """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."""
419
420
421
        return self._local_changes

    def clear_local_changes(self):
422
        """Reverts all local changes"""
423
424
425
        self._local_changes = {}

    def get_local_value(self, identifier):
426
427
428
429
430
431
        """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
           """
432
433
434
        return isc.cc.data.find_no_exc(self._local_changes, identifier)
        
    def get_current_value(self, identifier):
435
436
437
438
439
        """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
        """
440
441
442
        return isc.cc.data.find_no_exc(self._current_config, identifier)
        
    def get_default_value(self, identifier):
443
444
445
446
447
448
        """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
        """
449
        try:
450
451
452
453
454
455
456
457
458
459
460
461
462
            if identifier[0] == '/':
                identifier = identifier[1:]
            module, sep, id = identifier.partition("/")
            # if there is a 'higher-level' list index specified, we need
            # to check if that list specification has a default that
            # overrides the more specific default in the final spec item
            # (ie. list_default = [1, 2, 3], list_item_spec=int, default=0)
            # def default list[1] should return 2, not 0
            id_parts = isc.cc.data.split_identifier(id)
            id_prefix = ""
            while len(id_parts) > 0:
                id_part = id_parts.pop(0)
                item_id, list_indices = isc.cc.data.split_identifier_list_indices(id_part)
463
                id_list = module + "/" + id_prefix + "/" + item_id
464
                id_prefix += "/" + id_part
465
                part_spec = find_spec_part(self._specifications[module].get_config_spec(), id_prefix)
466
                if part_spec['item_type'] == 'named_set':
Jelte Jansen's avatar
Jelte Jansen committed
467
468
469
470
471
472
                    # For named sets, the identifier is partly defined
                    # by which values are actually present, and not
                    # purely by the specification.
                    # So if there is a part of the identifier left,
                    # we need to look up the value, then see if that
                    # contains the next part of the identifier we got
473
474
475
476
477
478
479
                    if len(id_parts) == 0:
                        if 'item_default' in part_spec:
                            return part_spec['item_default']
                        else:
                            return None
                    id_part = id_parts.pop(0)

480
481
                    named_set_value, type = self.get_value(id_list)
                    if id_part in named_set_value:
482
483
484
485
486
                        if len(id_parts) > 0:
                            # we are looking for the *default* value.
                            # so if not present in here, we need to
                            # lookup the one from the spec
                            rest_of_id = "/".join(id_parts)
487
                            result = isc.cc.data.find_no_exc(named_set_value[id_part], rest_of_id)
488
489
490
491
492
493
                            if result is None:
                                spec_part = self.find_spec_part(identifier)
                                if 'item_default' in spec_part:
                                    return spec_part['item_default']
                            return result
                        else:
494
                            return named_set_value[id_part]
495
496
497
                    else:
                        return None
                elif list_indices is not None:
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
                    # there's actually two kinds of default here for
                    # lists; they can have a default value (like an
                    # empty list), but their elements can  also have
                    # default values.
                    # So if the list item *itself* is a default,
                    # we need to get the value out of that. If not, we
                    # need to find the default for the specific element.
                    list_value, type = self.get_value(id_list) 
                    list_spec = find_spec_part(self._specifications[module].get_config_spec(), id_prefix)
                    if type == self.DEFAULT:
                        if 'item_default' in list_spec:
                            list_value = list_spec['item_default']
                            for i in list_indices:
                                if i < len(list_value):
                                    list_value = list_value[i]
                                else:
                                    # out of range, return None
                                    return None
                                
                            if len(id_parts) > 0:
                                rest_of_id = "/".join(id_parts)
                                return isc.cc.data.find(list_value, rest_of_id)
520
                            else:
521
                                return list_value
522
523
524
525
526
527
528
529
530
                    else:
                        # we do have a non-default list, see if our indices
                        # exist
                        for i in list_indices:
                            if i < len(list_value):
                                list_value = list_value[i]
                            else:
                                # out of range, return None
                                return None
531
                    
532
533
            spec = find_spec_part(self._specifications[module].get_config_spec(), id)
            if 'item_default' in spec:
534
535
536
                # one special case, named_set
                if spec['item_type'] == 'named_set':
                    print("is " + id_part + " in named set?")
537
538
539
                    return spec['item_default']
                else:
                    return spec['item_default']
540
541
            else:
                return None
542

543
544
545
        except isc.cc.data.DataNotFoundError as dnfe:
            return None

546
    def get_value(self, identifier, default = True):
547
548
549
550
551
        """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
552
553
554
           specification, or not found at all). Does not check and
           set DEFAULT if the argument 'default' is False (default
           defaults to True)"""
555
        value = self.get_local_value(identifier)
556
        if value != None:
557
558
            return value, self.LOCAL
        value = self.get_current_value(identifier)
559
        if value != None:
560
            return value, self.CURRENT
561
562
        if default:
            value = self.get_default_value(identifier)
563
            if value is not None:
564
                return value, self.DEFAULT
565
566
        return None, self.NONE

567
568
    def _append_value_item(self, result, spec_part, identifier, all, first = False):
        # Look at the spec; it is a list of items, or a map containing 'item_name' etc
569
        if type(spec_part) == list:
570
571
572
            for spec_part_element in spec_part:
                spec_part_element_name = spec_part_element['item_name']
                self._append_value_item(result, spec_part_element, identifier + "/" + spec_part_element_name, all)
573
        elif type(spec_part) == dict:
574
575
576
577
578
579
580
581
            # depending on item type, and the value of argument 'all'
            # we need to either add an item, or recursively go on
            # In the case of a list that is empty, we do need to show that
            item_name = spec_part['item_name']
            item_type = spec_part['item_type']
            if item_type == "list" and (all or first):
                spec_part_list = spec_part['list_item_spec']
                list_value, status = self.get_value(identifier)
582
583
                # If not set, and no default, lists will show up as 'None',
                # but it's better to treat it as an empty list then
584
                if list_value is None:
585
                    list_value = []
586

587
588
589
590
591
592
593
594
595
596
597
598
599
600
                if type(list_value) != list:
                    # the identifier specified a single element
                    self._append_value_item(result, spec_part_list, identifier, all)
                else:
                    list_len = len(list_value)
                    if len(list_value) == 0 and (all or first):
                        entry = _create_value_map_entry(identifier,
                                                        item_type,
                                                        [], status)
                        result.append(entry)
                    else:
                        for i in range(len(list_value)):
                            self._append_value_item(result, spec_part_list, "%s[%d]" % (identifier, i), all)
            elif item_type == "map":
601
                value, status = self.get_value(identifier)
602
603
                # just show the specific contents of a map, we are
                # almost never interested in just its name
604
                spec_part_map = spec_part['map_item_spec']
605
                self._append_value_item(result, spec_part_map, identifier, all)
606
            elif item_type == "named_set":
607
                value, status = self.get_value(identifier)
608

609
610
                # show just the one entry, when either the map is empty,
                # or when this is element is not requested specifically
611
                if len(value.keys()) == 0:
612
613
614
615
616
617
618
619
620
621
                    entry = _create_value_map_entry(identifier,
                                                    item_type,
                                                    {}, status)
                    result.append(entry)
                elif not first and not all:
                    entry = _create_value_map_entry(identifier,
                                                    item_type,
                                                    None, status)
                    result.append(entry)
                else:
622
                    spec_part_named_set = spec_part['named_set_item_spec']
623
                    for entry in value:
624
                        self._append_value_item(result,
625
                                                spec_part_named_set,
626
627
                                                identifier + "/" + entry,
                                                all)
628
            else:
629
                value, status = self.get_value(identifier)
630
                if status == self.NONE and not spec_part['item_optional']:
631
                    raise isc.cc.data.DataNotFoundError(identifier + " not found")
632

633
634
635
636
637
638
                entry = _create_value_map_entry(identifier,
                                                item_type,
                                                value, status)
                result.append(entry)
        return

639

640
    def get_value_maps(self, identifier = None, all = False):
641
642
643
644
        """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
645
646
647
648
           modified: true if the value is a local change that has not
                     been committed
           default: true if the value has not been changed (i.e. the
                    value is the default from the specification)
649
           TODO: use the consts for those last ones
650
           Throws DataNotFoundError if the identifier is bad
651
652
        """
        result = []
653
        if not identifier or identifier == "/":
654
655
            # No identifier, so we need the list of current modules
            for module in self._specifications.keys():
656
657
658
659
660
661
662
663
                if all:
                    spec = self.get_module_spec(module)
                    if spec:
                        spec_part = spec.get_config_spec()
                        self._append_value_item(result, spec_part, module, all, True)
                else:
                    entry = _create_value_map_entry(module, 'module', None)
                    result.append(entry)
664
        else:
665
            # Strip off start and end slashes, if they are there
666
            if len(identifier) > 0 and identifier[0] == '/':
667
                identifier = identifier[1:]
668
            if len(identifier) > 0 and identifier[-1] == '/':
669
                identifier = identifier[:-1]
670
            module, sep, id = identifier.partition('/')
Jelte Jansen's avatar
Jelte Jansen committed
671
            spec = self.get_module_spec(module)
672
            if spec:
673
                spec_part = find_spec_part(spec.get_config_spec(), id)
674
                self._append_value_item(result, spec_part, identifier, all, True)
675
676
        return result

677
678
679
680
681
682
683
684
685
686
    def unset(self, identifier):
        """
        Reset the value to default.
        """
        spec_part = self.find_spec_part(identifier)
        if spec_part is not None:
            isc.cc.data.unset(self._local_changes, identifier)
        else:
            raise isc.cc.data.DataNotFoundError(identifier + "not found")

687
    def set_value(self, identifier, value):
688
689
690
        """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
691
        spec_part = self.find_spec_part(identifier)
692
693
694
695
696
697
698
699
        if spec_part is not None:
            if value is not None:
                id, list_indices = isc.cc.data.split_identifier_list_indices(identifier)
                if list_indices is not None \
                   and spec_part['item_type'] == 'list':
                    spec_part = spec_part['list_item_spec']
                check_type(spec_part, value)
        else:
700
            raise isc.cc.data.DataNotFoundError(identifier + " not found")
701
702
703
704
705
706
707
708
709

        # Since we do not support list diffs (yet?), we need to
        # copy the currently set list of items to _local_changes
        # if we want to modify an element in there
        # (for any list indices specified in the full identifier)
        id_parts = isc.cc.data.split_identifier(identifier)
        cur_id_part = '/'
        for id_part in id_parts:
            id, list_indices = isc.cc.data.split_identifier_list_indices(id_part)
710
711
            cur_value, status = self.get_value(cur_id_part + id)
            # Check if the value was there in the first place
712
713
714
715
716
717
718
719
            # If we are at the final element, we do not care whether we found
            # it, since if we have reached this point and it did not exist,
            # it was apparently an optional value without a default.
            if status == MultiConfigData.NONE and cur_id_part != "/" and\
               cur_id_part + id != identifier:
                raise isc.cc.data.DataNotFoundError(id_part +
                                                    " not found in " +
                                                    cur_id_part)
720
            if list_indices is not None:
721
722
723
724
725
726
727
728
729
                # And check if we don't set something outside of any
                # list
                cur_list = cur_value
                for list_index in list_indices:
                    if list_index >= len(cur_list):
                        raise isc.cc.data.DataNotFoundError("No item " +
                                  str(list_index) + " in " + id_part)
                    else:
                        cur_list = cur_list[list_index]
730
731
732
                if status != MultiConfigData.LOCAL:
                    isc.cc.data.set(self._local_changes,
                                    cur_id_part + id,
733
                                    cur_value)
734
            cur_id_part = cur_id_part + id_part + "/"
735
736
737
738
739
740
741
742
743

            # We also need to copy to local if we are changing a named set,
            # so that the other items in the set do not disappear
            if spec_part_is_named_set(self.find_spec_part(cur_id_part)):
                ns_value, ns_status = self.get_value(cur_id_part)
                if ns_status != MultiConfigData.LOCAL:
                    isc.cc.data.set(self._local_changes,
                                    cur_id_part,
                                    ns_value)
744
        isc.cc.data.set(self._local_changes, identifier, value)
745
746
747

    def _get_list_items(self, item_name):
        """This method is used in get_config_item_list, to add list
748
749
           indices and named_set names to the completion list. If
           the given item_name is for a list or named_set, it'll
750
751
752
753
           return a list of those (appended to item_name), otherwise
           the list will only contain the item_name itself."""
        spec_part = self.find_spec_part(item_name)
        if 'item_type' in spec_part and \
754
           spec_part['item_type'] == 'named_set':
755
            subslash = ""
756
757
            if spec_part['named_set_item_spec']['item_type'] == 'map' or\
               spec_part['named_set_item_spec']['item_type'] == 'named_set':
758
759
760
761
762
763
764
765
766
                subslash = "/"
            values, status = self.get_value(item_name)
            if len(values) > 0:
                return [ item_name + "/" + v + subslash for v in values.keys() ]
            else:
                return [ item_name ]
        else:
            return [ item_name ]

767
    def get_config_item_list(self, identifier = None, recurse = False):
768
769
770
        """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
771
772
           the identifier (up to the first /) is interpreted as the
           module name"""
773
        if identifier and identifier != "/":
Jelte Jansen's avatar
Jelte Jansen committed
774
775
            if identifier.startswith("/"):
                identifier = identifier[1:]
776
            spec = self.find_spec_part(identifier)
777
778
779
780
781
            spec_list = spec_name_list(spec, identifier + "/", recurse)
            result_list = []
            for spec_name in spec_list:
                result_list.extend(self._get_list_items(spec_name))
            return result_list
782
        else:
783
784
            if recurse:
                id_list = []
785
786
                for module in self._specifications.keys():
                    id_list.extend(spec_name_list(self.find_spec_part(module), module, recurse))
787
788
789
                return id_list
            else:
                return list(self._specifications.keys())