ccsession.py 31.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Copyright (C) 2009  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.

#
# Client-side functionality for configuration and commands
#
# It keeps a cc-channel session with the configuration manager daemon,
# and handles configuration updates and direct commands

# modeled after ccsession.h/cc 'protocol' changes here need to be
# made there as well
24
25
"""Classes and functions for handling configuration and commands

26
27
28
   This module provides the ModuleCCSession and UIModuleCCSession
   classes, as well as a set of utility functions to create and parse
   messages related to commands and configuration
29
30
31
32
33

   Modules should use the ModuleCCSession class to connect to the
   configuration manager, and receive updates and commands from
   other modules.

34
35
36
   Configuration user interfaces should use the UIModuleCCSession
   to connect to b10-cmdctl, and receive and send configuration and
   commands through that to the configuration manager.
37
"""
38

39
from isc.cc import Session
40
from isc.config.config_data import ConfigData, MultiConfigData, BIND10_CONFIG_DATA_VERSION
41
import isc.config.module_spec
42
import isc
43
44
from isc.util.file import path_search
import bind10_config
45
from isc.log import log_config_update
46
import json
47
from isc.log_messages.config_messages import *
48
49

logger = isc.log.Logger("config")
50

51
class ModuleCCSessionError(Exception): pass
52
53

def parse_answer(msg):
Jelte Jansen's avatar
Jelte Jansen committed
54
55
56
    """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"""
Jelte Jansen's avatar
Jelte Jansen committed
57
58
    if type(msg) != dict:
        raise ModuleCCSessionError("Answer message is not a dict: " + str(msg))
59
    if 'result' not in msg:
60
        raise ModuleCCSessionError("answer message does not contain 'result' element")
61
    elif type(msg['result']) != list:
62
        raise ModuleCCSessionError("wrong result type in answer message")
63
    elif len(msg['result']) < 1:
64
        raise ModuleCCSessionError("empty result list in answer message")
65
    elif type(msg['result'][0]) != int:
66
        raise ModuleCCSessionError("wrong rcode type in answer message")
67
68
    else:
        if len(msg['result']) > 1:
Jelte Jansen's avatar
Jelte Jansen committed
69
70
            if (msg['result'][0] != 0 and type(msg['result'][1]) != str):
                raise ModuleCCSessionError("rcode in answer message is non-zero, value is not a string")
71
72
73
74
75
76
77
78
79
80
            return msg['result'][0], msg['result'][1]
        else:
            return msg['result'][0], None

def create_answer(rcode, arg = None):
    """Creates an answer packet for config&commands. rcode must be an
       integer. If rcode == 0, arg is an optional value that depends
       on what the command or option was. If rcode != 0, arg must be
       a string containing an error message"""
    if type(rcode) != int:
81
        raise ModuleCCSessionError("rcode in create_answer() must be an integer")
82
    if rcode != 0 and type(arg) != str:
83
        raise ModuleCCSessionError("arg in create_answer for rcode != 0 must be a string describing the error")
84
    if arg != None:
85
86
        return { 'result': [ rcode, arg ] }
    else:
87
        return { 'result': [ rcode ] }
88

89
90
# 'fixed' commands
"""Fixed names for command and configuration messages"""
Jelte Jansen's avatar
Jelte Jansen committed
91
COMMAND_CONFIG_UPDATE = "config_update"
Jelte Jansen's avatar
Jelte Jansen committed
92
COMMAND_MODULE_SPECIFICATION_UPDATE = "module_specification_update"
93
94

COMMAND_GET_COMMANDS_SPEC = "get_commands_spec"
95
COMMAND_GET_STATISTICS_SPEC = "get_statistics_spec"
96
97
98
COMMAND_GET_CONFIG = "get_config"
COMMAND_SET_CONFIG = "set_config"
COMMAND_GET_MODULE_SPEC = "get_module_spec"
99
COMMAND_MODULE_SPEC = "module_spec"
100
COMMAND_SHUTDOWN = "shutdown"
101
COMMAND_MODULE_STOPPING = "stopping"
102
103
104
105
106
107
108

def parse_command(msg):
    """Parses what may be a command message. If it looks like one,
       the function returns (command, value) where command is a
       string. If it is not, this function returns None, None"""
    if type(msg) == dict and len(msg.items()) == 1:
        cmd, value = msg.popitem()
109
        if cmd == "command" and type(value) == list:
Jelte Jansen's avatar
Jelte Jansen committed
110
            if len(value) == 1 and type(value[0]) == str:
111
                return value[0], None
Jelte Jansen's avatar
Jelte Jansen committed
112
            elif len(value) > 1 and type(value[0]) == str:
113
                return value[0], value[1]
114
115
116
117
118
119
120
    return None, None

def create_command(command_name, params = None):
    """Creates a module command message with the given command name (as
       specified in the module's specification, and an optional params
       object"""
    # TODO: validate_command with spec
Jelte Jansen's avatar
Jelte Jansen committed
121
122
    if type(command_name) != str:
        raise ModuleCCSessionError("command in create_command() not a string")
123
124
125
126
127
128
    cmd = [ command_name ]
    if params:
        cmd.append(params)
    msg = { 'command': cmd }
    return msg

129
def default_logconfig_handler(new_config, config_data):
130
131
132
    errors = []

    if config_data.get_module_spec().validate_config(False, new_config, errors):
133
134
        isc.log.log_config_update(json.dumps(new_config),
            json.dumps(config_data.get_module_spec().get_full_spec()))
135
    else:
136
        logger.error(CONFIG_LOG_CONFIG_ERRORS, errors)
137

138
class ModuleCCSession(ConfigData):
Jelte Jansen's avatar
Jelte Jansen committed
139
140
141
    """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
142
       options, and commands. It also gives the ModuleCCSession two callback
Jelte Jansen's avatar
Jelte Jansen committed
143
144
145
       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
146
       ModuleCCSession"""
Jelte Jansen's avatar
Jelte Jansen committed
147
       
148
149
150
    def __init__(self, spec_file_name, config_handler, command_handler,
                 cc_session=None, handle_logging_config=True,
                 socket_file = None):
151
        """Initialize a ModuleCCSession. This does *NOT* send the
Jelte Jansen's avatar
Jelte Jansen committed
152
           specification and request the configuration yet. Use start()
153
           for that once the ModuleCCSession has been initialized.
154
155
156

           specfile_name is the path to the specification file.

Jelte Jansen's avatar
Jelte Jansen committed
157
158
           config_handler and command_handler are callback functions,
           see set_config_handler and set_command_handler for more
159
160
161
162
163
164
165
166
167
168
169
170
           information on their signatures.

           cc_session can be used to pass in an existing CCSession,
           if it is None, one will be set up. This is mainly intended
           for testing purposes.

           handle_logging_config: if True, the module session will
           automatically handle logging configuration for the module;
           it will read the system-wide Logging configuration and call
           the logger manager to apply it. It will also inform the
           logger manager when the logging configuration gets updated.
           The module does not need to do anything except intializing
171
           its loggers, and provide log messages. Defaults to true.
172
173
174
175
176
177

           socket_file: If cc_session was none, this optional argument
           specifies which socket file to use to connect to msgq. It
           will be overridden by the environment variable
           MSGQ_SOCKET_FILE. If none, and no environment variable is
           set, it will use the system default.
178
        """
179
180
181
182
        module_spec = isc.config.module_spec_from_file(spec_file_name)
        ConfigData.__init__(self, module_spec)
        
        self._module_name = module_spec.get_module_name()
183
        
184
185
        self.set_config_handler(config_handler)
        self.set_command_handler(command_handler)
186

Jelte Jansen's avatar
Jelte Jansen committed
187
        if not cc_session:
188
            self._session = Session(socket_file)
Jelte Jansen's avatar
Jelte Jansen committed
189
190
        else:
            self._session = cc_session
191
        self._session.group_subscribe(self._module_name, "*")
192

193
        self._remote_module_configs = {}
194
195
196
197
198
        self._remote_module_callbacks = {}

        if handle_logging_config:
            self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS),
                                   default_logconfig_handler)
199

200
    def __del__(self):
201
202
203
        # If the CC Session obejct has been closed, it returns
        # immediately.
        if self._session._closed: return
204
205
206
207
        self._session.group_unsubscribe(self._module_name, "*")
        for module_name in self._remote_module_configs:
            self._session.group_unsubscribe(module_name)

208
    def start(self):
Jelte Jansen's avatar
Jelte Jansen committed
209
210
211
        """Send the specification for this module to the configuration
           manager, and request the current non-default configuration.
           The config_handler will be called with that configuration"""
212
        self.__send_spec()
213
        self.__request_config()
214

Jelte Jansen's avatar
Jelte Jansen committed
215
216
217
218
219
    def send_stopping(self):
        """Sends a 'stopping' message to the configuration manager. This
           message is just an FYI, and no response is expected. Any errors
           when sending this message (for instance if the msgq session has
           previously been closed) are logged, but ignored."""
220
221
222
        # create_command could raise an exception as well, but except for
        # out of memory related errors, these should all be programming
        # failures and are not caught
Jelte Jansen's avatar
Jelte Jansen committed
223
224
225
226
        msg = create_command(COMMAND_MODULE_STOPPING,
                             self.get_module_spec().get_full_spec())
        try:
            self._session.group_sendmsg(msg, "ConfigManager")
227
        except Exception as se:
Jelte Jansen's avatar
Jelte Jansen committed
228
229
230
            # If the session was previously closed, obvously trying to send
            # a message fails. (TODO: check if session is open so we can
            # error on real problems?)
231
            logger.error(CONFIG_SESSION_STOPPING_FAILED, se)
232

233
    def get_socket(self):
234
235
236
237
238
        """Returns the socket from the command channel session. This
           should *only* be used for select() loops to see if there
           is anything on the channel. If that loop is not completely
           time-critical, it is strongly recommended to only use
           check_command(), and not look at the socket at all."""
239
        return self._session._socket
240

241
    def close(self):
Jelte Jansen's avatar
Jelte Jansen committed
242
        """Close the session to the command channel"""
243
244
        self._session.close()

245
    def check_command(self, nonblock=True):
246
        """Check whether there is a command or configuration update on
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
           the channel. This function does a read on the cc session, and
           returns nothing.
           It calls check_command_without_recvmsg()
           to parse the received message.
           
           If nonblock is True, it just checks if there's a command
           and does nothing if there isn't. If nonblock is False, it
           waits until it arrives. It temporarily sets timeout to infinity,
           because commands may not come in arbitrary long time."""
        timeout_orig = self._session.get_timeout()
        self._session.set_timeout(0)
        try:
            msg, env = self._session.group_recvmsg(nonblock)
        finally:
            self._session.set_timeout(timeout_orig)
262
263
264
265
266
267
268
        self.check_command_without_recvmsg(msg, env)

    def check_command_without_recvmsg(self, msg, env):
        """Parse the given message to see if there is a command or a
           configuration update. Calls the corresponding handler
           functions if present. Responds on the channel if the
           handler returns a message.""" 
269
        # should we default to an answer? success-by-default? unhandled error?
270
        if msg is not None and not 'result' in msg:
Jelte Jansen's avatar
Jelte Jansen committed
271
272
            answer = None
            try:
273
                module_name = env['group']
Jelte Jansen's avatar
Jelte Jansen committed
274
275
276
                cmd, arg = isc.config.ccsession.parse_command(msg)
                if cmd == COMMAND_CONFIG_UPDATE:
                    new_config = arg
277
278
279
280
281
282
283
284
285
                    # If the target channel was not this module
                    # it might be in the remote_module_configs
                    if module_name != self._module_name:
                        if module_name in self._remote_module_configs:
                            # no checking for validity, that's up to the
                            # module itself.
                            newc = self._remote_module_configs[module_name].get_local_config()
                            isc.cc.data.merge(newc, new_config)
                            self._remote_module_configs[module_name].set_local_config(newc)
286
287
288
                            if self._remote_module_callbacks[module_name] != None:
                                self._remote_module_callbacks[module_name](new_config,
                                                                           self._remote_module_configs[module_name])
289
290
                        # For other modules, we're not supposed to answer
                        return
291
292

                    # ok, so apparently this update is for us.
293
294
295
296
                    errors = []
                    if not self._config_handler:
                        answer = create_answer(2, self._module_name + " has no config handler")
                    elif not self.get_module_spec().validate_config(False, new_config, errors):
297
                        answer = create_answer(1, ", ".join(errors))
298
                    else:
Jelte Jansen's avatar
Jelte Jansen committed
299
                        isc.cc.data.remove_identical(new_config, self.get_local_config())
Jelte Jansen's avatar
Jelte Jansen committed
300
                        answer = self._config_handler(new_config)
Jelte Jansen's avatar
Jelte Jansen committed
301
302
303
304
305
                        rcode, val = parse_answer(answer)
                        if rcode == 0:
                            newc = self.get_local_config()
                            isc.cc.data.merge(newc, new_config)
                            self.set_local_config(newc)
Jelte Jansen's avatar
Jelte Jansen committed
306
                else:
307
308
309
310
311
312
                    # ignore commands for 'remote' modules
                    if module_name == self._module_name:
                        if self._command_handler:
                            answer = self._command_handler(cmd, arg)
                        else:
                            answer = create_answer(2, self._module_name + " has no command handler")
Jelte Jansen's avatar
Jelte Jansen committed
313
314
315
316
            except Exception as exc:
                answer = create_answer(1, str(exc))
            if answer:
                self._session.group_reply(env, answer)
317
    
318
    def set_config_handler(self, config_handler):
319
320
        """Set the config handler for this module. The handler is a
           function that takes the full configuration and handles it.
321
           It should return an answer created with create_answer()"""
322
323
324
        self._config_handler = config_handler
        # should we run this right now since we've changed the handler?

325
    def set_command_handler(self, command_handler):
326
327
        """Set the command handler for this module. The handler is a
           function that takes a command as defined in the .spec file
328
           and return an answer created with create_answer()"""
329
330
        self._command_handler = command_handler

331
    def _add_remote_config_internal(self, module_spec, config_update_callback=None):
332
        """The guts of add_remote_config and add_remote_config_by_name"""
333
334
        module_cfg = ConfigData(module_spec)
        module_name = module_spec.get_module_name()
335

336
        self._session.group_subscribe(module_name)
337
338

        # Get the current config for that module now
339
        seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager")
340
341
342
343
344
345
346

        try:
            answer, env = self._session.group_recvmsg(False, seq)
        except isc.cc.SessionTimeout:
            raise ModuleCCSessionError("No answer from ConfigManager when "
                                       "asking about Remote module " +
                                       module_name)
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
347
        call_callback = False
348
349
350
        if answer:
            rcode, value = parse_answer(answer)
            if rcode == 0:
351
                if value != None and module_spec.validate_config(False, value):
352
                    module_cfg.set_local_config(value)
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
353
                    call_callback = True
354
355
356

        # all done, add it
        self._remote_module_configs[module_name] = module_cfg
357
        self._remote_module_callbacks[module_name] = config_update_callback
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
358
359
        if call_callback and config_update_callback is not None:
            config_update_callback(value, module_cfg)
360

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
    def add_remote_config_by_name(self, module_name,
                                  config_update_callback=None):
        """
        This does the same as add_remote_config, but you provide the module name
        instead of the name of the spec file.
        """
        seq = self._session.group_sendmsg(create_command(COMMAND_GET_MODULE_SPEC,
                                                         { "module_name":
                                                         module_name }),
                                          "ConfigManager")
        try:
            answer, env = self._session.group_recvmsg(False, seq)
        except isc.cc.SessionTimeout:
            raise ModuleCCSessionError("No answer from ConfigManager when " +
                                       "asking about for spec of Remote " +
                                       "module " + module_name)
        if answer:
            rcode, value = parse_answer(answer)
            if rcode == 0:
                module_spec = isc.config.module_spec.ModuleSpec(value)
                if module_spec.get_module_name() != module_name:
                    raise ModuleCCSessionError("Module name mismatch: " +
                                               module_name + " and " +
                                               module_spec.get_module_name())
                self._add_remote_config_internal(module_spec,
                                                 config_update_callback)
            else:
                raise ModuleCCSessionError("Error code " + str(rcode) +
                                           "when asking for module spec of " +
                                           module_name)
        else:
            raise ModuleCCSessionError("No answer when asking for module " +
                                       "spec of " + module_name)
        # Just to be consistent with the add_remote_config
        return module_name

    def add_remote_config(self, spec_file_name, config_update_callback=None):
398
399
400
401
402
403
404
405
406
407
408
409
410
411
        """Gives access to the configuration of a different module.
           These remote module options can at this moment only be
           accessed through get_remote_config_value(). This function
           also subscribes to the channel of the remote module name
           to receive the relevant updates. It is not possible to
           specify your own handler for this right now, but you can
           specify a callback that is called after the change happened.
           start() must have been called on this CCSession
           prior to the call to this method.
           Returns the name of the module."""
        module_spec = isc.config.module_spec_from_file(spec_file_name)
        self._add_remote_config_internal(module_spec, config_update_callback)
        return module_spec.get_module_name()

412
413
414
    def remove_remote_config(self, module_name):
        """Removes the remote configuration access for this module"""
        if module_name in self._remote_module_configs:
415
            self._session.group_unsubscribe(module_name)
416
            del self._remote_module_configs[module_name]
417
            del self._remote_module_callbacks[module_name]
418
419
420
421
422
423
424
425
426
427
428

    def get_remote_config_value(self, module_name, identifier):
        """Returns the current setting for the given identifier at the
           given module. If the module has not been added with
           add_remote_config, a ModuleCCSessionError is raised"""
        if module_name in self._remote_module_configs:
            return self._remote_module_configs[module_name].get_value(identifier)
        else:
            raise ModuleCCSessionError("Remote module " + module_name +
                                       " not found")

429
    def __send_spec(self):
430
        """Sends the data specification to the configuration manager"""
431
        msg = create_command(COMMAND_MODULE_SPEC, self.get_module_spec().get_full_spec())
432
        seq = self._session.group_sendmsg(msg, "ConfigManager")
433
434
435
436
437
        try:
            answer, env = self._session.group_recvmsg(False, seq)
        except isc.cc.SessionTimeout:
            # TODO: log an error?
            pass
438

439
    def __request_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
440
441
        """Asks the configuration manager for the current configuration, and call the config handler if set.
           Raises a ModuleCCSessionError if there is no answer from the configuration manager"""
442
        seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": self._module_name }), "ConfigManager")
443
444
445
446
447
        try:
            answer, env = self._session.group_recvmsg(False, seq)
            if answer:
                rcode, value = parse_answer(answer)
                if rcode == 0:
448
                    errors = []
449
450
451
452
                    if value != None:
                        if self.get_module_spec().validate_config(False,
                                                                  value,
                                                                  errors):
453
                            self.set_local_config(value)
454
455
456
457
458
459
                            if self._config_handler:
                                self._config_handler(value)
                        else:
                            raise ModuleCCSessionError(
                                "Wrong data in configuration: " +
                                " ".join(errors))
460
                else:
461
                    logger.error(CONFIG_GET_FAILED, value)
Jelte Jansen's avatar
Jelte Jansen committed
462
            else:
463
464
465
                raise ModuleCCSessionError("No answer from configuration manager")
        except isc.cc.SessionTimeout:
            raise ModuleCCSessionError("CC Session timeout waiting for configuration manager")
466

467

468
469
470
class UIModuleCCSession(MultiConfigData):
    """This class is used in a configuration user interface. It contains
       specific functions for getting, displaying, and sending
471
       configuration settings through the b10-cmdctl module."""
472
    def __init__(self, conn):
473
474
        """Initialize a UIModuleCCSession. The conn object that is
           passed must have send_GET and send_POST functions"""
475
476
        MultiConfigData.__init__(self)
        self._conn = conn
477
        self.update_specs_and_config()
478
479

    def request_specifications(self):
480
481
482
483
484
485
        """Clears the current list of specifications, and requests a new
            list from b10-cmdctl. As other actions may have caused modules
            to be stopped, or new modules to be added, this is expected to
            be run after each interaction (at this moment). It is usually
            also combined with request_current_config(). For that reason,
            we provide update_specs_and_config() which calls both."""
Jelte Jansen's avatar
Jelte Jansen committed
486
        specs = self._conn.send_GET('/module_spec')
487
        self.clear_specifications()
488
        for module in specs.keys():
Jelte Jansen's avatar
Jelte Jansen committed
489
            self.set_specification(isc.config.ModuleSpec(specs[module]))
490
491

    def request_current_config(self):
492
        """Requests the current configuration from the configuration
493
494
495
           manager through b10-cmdctl, and stores those as CURRENT. This
           does not modify any local changes, it just updates to the current
           state of the server itself."""
496
        config = self._conn.send_GET('/config_data')
497
        if 'version' not in config or config['version'] != BIND10_CONFIG_DATA_VERSION:
498
            raise ModuleCCSessionError("Bad config version")
499
        self._set_current_config(config)
500

501
502
503
504
505
506
507
508
    def update_specs_and_config(self):
        """Convenience function to both clear and update the known list of
           module specifications, and update the current configuration on
           the server side. There are a few cases where the caller might only
           want to run one of these tasks, but often they are both needed."""
        self.request_specifications()
        self.request_current_config()

509
    def _add_value_to_list(self, identifier, value, module_spec):
510
511
512
        cur_list, status = self.get_value(identifier)
        if not cur_list:
            cur_list = []
513

514
        if value is None:
515
516
517
518
            if "item_default" in module_spec["list_item_spec"]:
                value = module_spec["list_item_spec"]["item_default"]

        if value is None:
519
520
            raise isc.cc.data.DataNotFoundError(
                "No value given and no default for " + str(identifier))
521

522
523
        if value not in cur_list:
            cur_list.append(value)
524
            self.set_value(identifier, cur_list)
525
        else:
526
            raise isc.cc.data.DataAlreadyPresentError(str(value) +
527
                                                      " already in "
528
                                                      + str(identifier))
529

530
    def _add_value_to_named_set(self, identifier, value, item_value):
531
532
533
534
535
536
537
538
539
        if type(value) != str:
            raise isc.cc.data.DataTypeError("Name for named_set " +
                                            identifier +
                                            " must be a string")
        # fail on both None and empty string
        if not value:
            raise isc.cc.data.DataNotFoundError(
                    "Need a name to add a new item to named_set " +
                    str(identifier))
540
541
542
543
        else:
            cur_map, status = self.get_value(identifier)
            if not cur_map:
                cur_map = {}
544
            if value not in cur_map:
545
                cur_map[value] = item_value
546
547
                self.set_value(identifier, cur_map)
            else:
548
                raise isc.cc.data.DataAlreadyPresentError(value +
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
549
550
                                                          " already in " +
                                                          identifier)
551

552
    def add_value(self, identifier, value_str = None, set_value_str = None):
553
554
        """Add a value to a configuration list. Raises a DataTypeError
           if the value does not conform to the list_item_spec field
555
556
           of the module config data specification. If value_str is
           not given, we add the default as specified by the .spec
557
558
559
560
           file. Raises a DataNotFoundError if the given identifier
           is not specified in the specification as a map or list.
           Raises a DataAlreadyPresentError if the specified element
           already exists."""
Jelte Jansen's avatar
Jelte Jansen committed
561
        module_spec = self.find_spec_part(identifier)
562
563
        if module_spec is None:
            raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))
564

565
        # the specified element must be a list or a named_set
566
        if 'list_item_spec' in module_spec:
567
568
569
570
571
572
573
574
            value = None
            # in lists, we might get the value with spaces, making it
            # the third argument. In that case we interpret both as
            # one big string meant as the value
            if value_str is not None:
                if set_value_str is not None:
                    value_str += set_value_str
                value = isc.cc.data.parse_value_str(value_str)
575
            self._add_value_to_list(identifier, value, module_spec)
576
        elif 'named_set_item_spec' in module_spec:
577
            item_name = None
578
            item_value = None
579
580
581
582
583
584
585
586
587
            if value_str is not None:
                item_name =  isc.cc.data.parse_value_str(value_str)
            if set_value_str is not None:
                item_value = isc.cc.data.parse_value_str(set_value_str)
            else:
                if 'item_default' in module_spec['named_set_item_spec']:
                    item_value = module_spec['named_set_item_spec']['item_default']
            self._add_value_to_named_set(identifier, item_name,
                                         item_value)
588
        else:
589
            raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list or a named set")
590

591
592
    def _remove_value_from_list(self, identifier, value):
        if value is None:
593
            # we are directly removing a list index
594
595
            id, list_indices = isc.cc.data.split_identifier_list_indices(identifier)
            if list_indices is None:
596
                raise isc.cc.data.DataTypeError("identifier in remove_value() does not contain a list index, and no value to remove")
597
598
599
600
601
602
            else:
                self.set_value(identifier, None)
        else:
            cur_list, status = self.get_value(identifier)
            if not cur_list:
                cur_list = []
603
            elif value in cur_list:
604
605
                cur_list.remove(value)
            self.set_value(identifier, cur_list)
606

607
    def _remove_value_from_named_set(self, identifier, value):
608
        if value is None:
609
            raise isc.cc.data.DataNotFoundError("Need a name to remove an item from named_set " + str(identifier))
610
        elif type(value) != str:
611
            raise isc.cc.data.DataTypeError("Name for named_set " + identifier + " must be a string")
612
613
614
615
616
617
        else:
            cur_map, status = self.get_value(identifier)
            if not cur_map:
                cur_map = {}
            if value in cur_map:
                del cur_map[value]
618
                self.set_value(identifier, cur_map)
619
            else:
620
                raise isc.cc.data.DataNotFoundError(value + " not found in named_set " + str(identifier))
621
622

    def remove_value(self, identifier, value_str):
623
        """Remove a value from a configuration list or named set.
624
625
626
627
        The value string must be a string representation of the full
        item. Raises a DataTypeError if the value at the identifier
        is not a list, or if the given value_str does not match the
        list_item_spec """
628
629
630
631
632
633
634
635
636
        module_spec = self.find_spec_part(identifier)
        if module_spec is None:
            raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))

        value = None
        if value_str is not None:
            value = isc.cc.data.parse_value_str(value_str)

        if 'list_item_spec' in module_spec:
637
638
            if value is not None:
                isc.config.config_data.check_type(module_spec['list_item_spec'], value)
639
            self._remove_value_from_list(identifier, value)
640
641
        elif 'named_set_item_spec' in module_spec:
            self._remove_value_from_named_set(identifier, value)
642
        else:
643
            raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list or a named_set")
644
645
646



647
    def commit(self):
648
649
        """Commit all local changes, send them through b10-cmdctl to
           the configuration manager"""
650
        if self.get_local_changes():
651
652
653
654
655
656
657
658
            response = self._conn.send_POST('/ConfigManager/set_config',
                                            [ self.get_local_changes() ])
            answer = isc.cc.data.parse_value_str(response.read().decode())
            # answer is either an empty dict (on success), or one
            # containing errors
            if answer == {}:
                self.clear_local_changes()
            elif "error" in answer:
659
                raise ModuleCCSessionError("Error: " + str(answer["error"]) + "\n" + "Configuration not committed")
660
661
            else:
                raise ModuleCCSessionError("Unknown format of answer in commit(): " + str(answer))