cfgmgr.py 16 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.

16 17 18 19 20
"""This is the BIND 10 configuration manager, run by b10-cfgmgr.

   It stores the system configuration, and sends updates of the
   configuration to the modules that need them.
"""
21

22
import isc
23 24 25 26
import signal
import ast
import pprint
import os
27
from isc.cc import data
28

Jelte Jansen's avatar
Jelte Jansen committed
29
class ConfigManagerDataReadError(Exception):
30 31
    """This exception is thrown when there is an error while reading
       the current configuration on startup."""
Jelte Jansen's avatar
Jelte Jansen committed
32 33 34
    pass

class ConfigManagerDataEmpty(Exception):
35 36
    """This exception is thrown when the currently stored configuration
       is not found, or appears empty."""
Jelte Jansen's avatar
Jelte Jansen committed
37 38
    pass

39
class ConfigManagerData:
40 41 42
    """This class hold the actual configuration information, and
       reads it from and writes it to persistent storage"""

43 44
    CONFIG_VERSION = 1

Jelte Jansen's avatar
Jelte Jansen committed
45 46 47 48 49
    def __init__(self, data_path, file_name = "b10-config.db"):
        """Initialize the data for the configuration manager, and
           set the version and path for the data store. Initializing
           this does not yet read the database, a call to
           read_from_file is needed for that."""
50 51
        self.data = {}
        self.data['version'] = ConfigManagerData.CONFIG_VERSION
Jelte Jansen's avatar
Jelte Jansen committed
52
        self.data_path = data_path
Jelte Jansen's avatar
Jelte Jansen committed
53
        self.db_filename = data_path + os.sep + file_name
54

Jelte Jansen's avatar
Jelte Jansen committed
55 56 57 58 59 60 61 62 63 64
    def read_from_file(data_path, file_name = "b10-config.db"):
        """Read the current configuration found in the file at
           data_path. If the file does not exist, a
           ConfigManagerDataEmpty exception is raised. If there is a
           parse error, or if the data in the file has the wrong
           version, a ConfigManagerDataReadError is raised. In the first
           case, it is probably safe to log and ignore. In the case of
           the second exception, the best way is probably to report the
           error and stop loading the system."""
        config = ConfigManagerData(data_path, file_name)
65
        try:
66
            file = open(config.db_filename, 'r')
67 68 69 70 71
            file_config = ast.literal_eval(file.read())
            if 'version' in file_config and \
                file_config['version'] == ConfigManagerData.CONFIG_VERSION:
                config.data = file_config
            else:
Jelte Jansen's avatar
Jelte Jansen committed
72
                # We can put in a migration path here for old data
73
                raise ConfigManagerDataReadError("[b10-cfgmgr] Old version of data found")
74 75
            file.close()
        except IOError as ioe:
Jelte Jansen's avatar
Jelte Jansen committed
76
            raise ConfigManagerDataEmpty("No config file found")
77
        except:
Jelte Jansen's avatar
Jelte Jansen committed
78
            raise ConfigManagerDataReadError("Config file unreadable")
79 80 81

        return config
        
Jelte Jansen's avatar
Jelte Jansen committed
82 83 84 85
    def write_to_file(self, output_file_name = None):
        """Writes the current configuration data to a file. If
           output_file_name is not specified, the file used in
           read_from_file is used."""
86
        try:
Jelte Jansen's avatar
Jelte Jansen committed
87
            tmp_filename = self.db_filename + ".tmp"
88 89 90 91 92 93
            file = open(tmp_filename, 'w');
            pp = pprint.PrettyPrinter(indent=4)
            s = pp.pformat(self.data)
            file.write(s)
            file.write("\n")
            file.close()
Jelte Jansen's avatar
Jelte Jansen committed
94 95 96 97
            if output_file_name:
                os.rename(tmp_filename, output_file_name)
            else:
                os.rename(tmp_filename, self.db_filename)
98
        except IOError as ioe:
Jelte Jansen's avatar
Jelte Jansen committed
99 100 101 102 103
            # TODO: log this (level critical)
            print("[b10-cfgmgr] Unable to write config file; configuration not stored: " + str(ioe))
        except OSError as ose:
            # TODO: log this (level critical)
            print("[b10-cfgmgr] Unable to write config file; configuration not stored: " + str(ose))
104

Jelte Jansen's avatar
Jelte Jansen committed
105 106 107 108 109 110 111
    def __eq__(self, other):
        """Returns True if the data contained is equal. data_path and
           db_filename may be different."""
        if type(other) != type(self):
            return False
        return self.data == other.data

112
class ConfigManager:
Jelte Jansen's avatar
Jelte Jansen committed
113 114 115 116 117 118 119
    """Creates a configuration manager. The data_path is the path
       to the directory containing the b10-config.db file.
       If session is set, this will be used as the communication
       channel session. If not, a new session will be created.
       The ability to specify a custom session is for testing purposes
       and should not be needed for normal usage."""
    def __init__(self, data_path, session = None):
120 121 122 123 124
        """Initialize the configuration manager. The data_path string
           is the path to the directory where the configuration is
           stored (in <data_path>/b10-config.db). Session is an optional
           cc-channel session. If this is not given, a new one is
           created"""
Jelte Jansen's avatar
Jelte Jansen committed
125
        self.data_path = data_path
Jelte Jansen's avatar
Jelte Jansen committed
126
        self.module_specs = {}
Jelte Jansen's avatar
Jelte Jansen committed
127
        self.config = ConfigManagerData(data_path)
Jelte Jansen's avatar
Jelte Jansen committed
128 129 130 131
        if session:
            self.cc = session
        else:
            self.cc = isc.cc.Session()
132 133 134 135 136
        self.cc.group_subscribe("ConfigManager")
        self.cc.group_subscribe("Boss", "ConfigManager")
        self.running = False

    def notify_boss(self):
Jelte Jansen's avatar
Jelte Jansen committed
137
        """Notifies the Boss module that the Config Manager is running"""
138 139
        self.cc.group_sendmsg({"running": "configmanager"}, "Boss")

Jelte Jansen's avatar
Jelte Jansen committed
140
    def set_module_spec(self, spec):
141
        """Adds a ModuleSpec"""
Jelte Jansen's avatar
Jelte Jansen committed
142
        self.module_specs[spec.get_module_name()] = spec
143

144 145 146 147 148 149
    def remove_module_spec(self, module_name):
        """Removes the full ModuleSpec for the given module_name.
           Does nothing if the module was not present."""
        if module_name in self.module_specs:
            del self.module_specs[module_name]

Jelte Jansen's avatar
Jelte Jansen committed
150
    def get_module_spec(self, module_name):
151 152
        """Returns the full ModuleSpec for the module with the given
           module_name"""
Jelte Jansen's avatar
Jelte Jansen committed
153 154
        if module_name in self.module_specs:
            return self.module_specs[module_name]
155

Jelte Jansen's avatar
Jelte Jansen committed
156
    def get_config_spec(self, name = None):
157
        """Returns a dict containing 'module_name': config_spec for
158 159 160 161
           all modules. If name is specified, only that module will
           be included"""
        config_data = {}
        if name:
Jelte Jansen's avatar
Jelte Jansen committed
162
            if name in self.module_specs:
Jelte Jansen's avatar
Jelte Jansen committed
163
                config_data[name] = self.module_specs[name].get_config_spec()
164
        else:
Jelte Jansen's avatar
Jelte Jansen committed
165 166
            for module_name in self.module_specs.keys():
                config_data[module_name] = self.module_specs[module_name].get_config_spec()
167 168
        return config_data

Jelte Jansen's avatar
Jelte Jansen committed
169
    def get_commands_spec(self, name = None):
170
        """Returns a dict containing 'module_name': commands_spec for
171 172 173 174
           all modules. If name is specified, only that module will
           be included"""
        commands = {}
        if name:
Jelte Jansen's avatar
Jelte Jansen committed
175
            if name in self.module_specs:
Jelte Jansen's avatar
Jelte Jansen committed
176
                commands[name] = self.module_specs[name].get_commands_spec()
177
        else:
Jelte Jansen's avatar
Jelte Jansen committed
178 179
            for module_name in self.module_specs.keys():
                commands[module_name] = self.module_specs[module_name].get_commands_spec()
180
        return commands
181 182

    def read_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
183 184
        """Read the current configuration from the b10-config.db file
           at the path specificied at init()"""
185 186 187 188 189
        try:
            self.config = ConfigManagerData.read_from_file(self.data_path)
        except ConfigManagerDataEmpty:
            # ok, just start with an empty config
            self.config = ConfigManagerData(self.data_path)
190 191
        
    def write_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
192 193
        """Write the current configuration to the b10-config.db file
           at the path specificied at init()"""
194 195
        self.config.write_to_file()

Jelte Jansen's avatar
Jelte Jansen committed
196
    def _handle_get_module_spec(self, cmd):
197
        """Private function that handles the 'get_module_spec' command"""
Jelte Jansen's avatar
Jelte Jansen committed
198
        answer = {}
199 200 201 202
        if cmd != None:
            if type(cmd) == dict:
                if 'module_name' in cmd and cmd['module_name'] != '':
                    module_name = cmd['module_name']
Jelte Jansen's avatar
Jelte Jansen committed
203
                    answer = isc.config.ccsession.create_answer(0, self.get_config_spec(module_name))
Jelte Jansen's avatar
Jelte Jansen committed
204
                else:
Jelte Jansen's avatar
Jelte Jansen committed
205
                    answer = isc.config.ccsession.create_answer(1, "Bad module_name in get_module_spec command")
Jelte Jansen's avatar
Jelte Jansen committed
206
            else:
Jelte Jansen's avatar
Jelte Jansen committed
207
                answer = isc.config.ccsession.create_answer(1, "Bad get_module_spec command, argument not a dict")
Jelte Jansen's avatar
Jelte Jansen committed
208
        else:
Jelte Jansen's avatar
Jelte Jansen committed
209
            answer = isc.config.ccsession.create_answer(0, self.get_config_spec())
Jelte Jansen's avatar
Jelte Jansen committed
210 211 212
        return answer

    def _handle_get_config(self, cmd):
213
        """Private function that handles the 'get_config' command"""
Jelte Jansen's avatar
Jelte Jansen committed
214
        answer = {}
215 216 217 218
        if cmd != None:
            if type(cmd) == dict:
                if 'module_name' in cmd and cmd['module_name'] != '':
                    module_name = cmd['module_name']
Jelte Jansen's avatar
Jelte Jansen committed
219
                    try:
220
                        answer = isc.config.ccsession.create_answer(0, data.find(self.config.data, module_name))
Jelte Jansen's avatar
Jelte Jansen committed
221 222 223
                    except data.DataNotFoundError as dnfe:
                        # no data is ok, that means we have nothing that
                        # deviates from default values
224
                        answer = isc.config.ccsession.create_answer(0, {})
Jelte Jansen's avatar
Jelte Jansen committed
225
                else:
226
                    answer = isc.config.ccsession.create_answer(1, "Bad module_name in get_config command")
Jelte Jansen's avatar
Jelte Jansen committed
227
            else:
228
                answer = isc.config.ccsession.create_answer(1, "Bad get_config command, argument not a dict")
Jelte Jansen's avatar
Jelte Jansen committed
229
        else:
230
            answer = isc.config.ccsession.create_answer(0, self.config.data)
Jelte Jansen's avatar
Jelte Jansen committed
231 232 233
        return answer

    def _handle_set_config(self, cmd):
234
        """Private function that handles the 'set_config' command"""
235
        answer = None
236 237 238
        if cmd == None:
            return isc.config.ccsession.create_answer(1, "Wrong number of arguments")
        if len(cmd) == 2:
Jelte Jansen's avatar
Jelte Jansen committed
239
            # todo: use api (and check the data against the definition?)
240
            module_name = cmd[0]
Jelte Jansen's avatar
Jelte Jansen committed
241 242
            conf_part = data.find_no_exc(self.config.data, module_name)
            if conf_part:
243
                data.merge(conf_part, cmd[1])
Jelte Jansen's avatar
Jelte Jansen committed
244 245
                update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, conf_part)
                self.cc.group_sendmsg(update_cmd, module_name)
246
                answer, env = self.cc.group_recvmsg(False)
Jelte Jansen's avatar
Jelte Jansen committed
247 248
            else:
                conf_part = data.set(self.config.data, module_name, {})
249
                data.merge(conf_part[module_name], cmd[1])
Jelte Jansen's avatar
Jelte Jansen committed
250
                # send out changed info
Jelte Jansen's avatar
Jelte Jansen committed
251 252
                update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, conf_part[module_name])
                self.cc.group_sendmsg(update_cmd, module_name)
253
                # replace 'our' answer with that of the module
254
                answer, env = self.cc.group_recvmsg(False)
Jelte Jansen's avatar
Jelte Jansen committed
255 256 257 258
            if answer:
                rcode, val = isc.config.ccsession.parse_answer(answer)
                if rcode == 0:
                    self.write_config()
259
        elif len(cmd) == 1:
Jelte Jansen's avatar
Jelte Jansen committed
260
            # todo: use api (and check the data against the definition?)
261
            old_data = self.config.data.copy()
262
            data.merge(self.config.data, cmd[0])
Jelte Jansen's avatar
Jelte Jansen committed
263
            # send out changed info
264
            got_error = False
265
            err_list = []
Jelte Jansen's avatar
Jelte Jansen committed
266 267
            for module in self.config.data:
                if module != "version":
Jelte Jansen's avatar
Jelte Jansen committed
268 269
                    update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, self.config.data[module])
                    self.cc.group_sendmsg(update_cmd, module)
270
                    answer, env = self.cc.group_recvmsg(False)
Jelte Jansen's avatar
Jelte Jansen committed
271
                    if answer == None:
272
                        got_error = True
Jelte Jansen's avatar
Jelte Jansen committed
273 274 275 276 277 278
                        err_list.append("No answer message from " + module)
                    else:
                        rcode, val = isc.config.ccsession.parse_answer(answer)
                        if rcode != 0:
                            got_error = True
                            err_list.append(val)
279 280
            if not got_error:
                self.write_config()
281 282
                answer = isc.config.ccsession.create_answer(0)
            else:
283 284
                # TODO rollback changes that did get through, should we re-send update?
                self.config.data = old_data
285
                answer = isc.config.ccsession.create_answer(1, " ".join(err_list))
Jelte Jansen's avatar
Jelte Jansen committed
286
        else:
287
            print(cmd)
288
            answer = isc.config.ccsession.create_answer(1, "Wrong number of arguments")
289
        if not answer:
Jelte Jansen's avatar
Jelte Jansen committed
290
            answer = isc.config.ccsession.create_answer(1, "No answer message from " + cmd[0])
291
            
Jelte Jansen's avatar
Jelte Jansen committed
292 293
        return answer

Jelte Jansen's avatar
Jelte Jansen committed
294
    def _handle_module_spec(self, spec):
295
        """Private function that handles the 'module_spec' command"""
Jelte Jansen's avatar
Jelte Jansen committed
296
        # todo: validate? (no direct access to spec as
Jelte Jansen's avatar
Jelte Jansen committed
297
        # todo: use ModuleSpec class
Jelte Jansen's avatar
Jelte Jansen committed
298 299
        # todo: error checking (like keyerrors)
        answer = {}
Jelte Jansen's avatar
Jelte Jansen committed
300
        self.set_module_spec(spec)
301 302 303
        
        # We should make one general 'spec update for module' that
        # passes both specification and commands at once
304 305 306 307 308 309
        spec_update = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_SPECIFICATION_UPDATE,
                                                          [ spec.get_module_name(), spec.get_config_spec() ])
        self.cc.group_sendmsg(spec_update, "Cmd-Ctrld")
        cmds_update = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_COMMANDS_UPDATE,
                                                          [ spec.get_module_name(), spec.get_commands_spec() ])
        self.cc.group_sendmsg(cmds_update, "Cmd-Ctrld")
310
        answer = isc.config.ccsession.create_answer(0)
Jelte Jansen's avatar
Jelte Jansen committed
311 312
        return answer

313
    def handle_msg(self, msg):
314
        """Handle a command from the cc channel to the configuration manager"""
315
        answer = {}
316
        print("[XX] got msg: " + str(msg))
317 318 319 320 321 322 323 324 325
        cmd, arg = isc.config.ccsession.parse_command(msg)
        if cmd:
            if cmd == isc.config.ccsession.COMMAND_GET_COMMANDS_SPEC:
                answer = isc.config.ccsession.create_answer(0, self.get_commands_spec())
            elif cmd == isc.config.ccsession.COMMAND_GET_MODULE_SPEC:
                answer = self._handle_get_module_spec(arg)
            elif cmd == isc.config.ccsession.COMMAND_GET_CONFIG:
                answer = self._handle_get_config(arg)
            elif cmd == isc.config.ccsession.COMMAND_SET_CONFIG:
326
                print("[b10-cfgmgr] got set_config command")
327 328
                answer = self._handle_set_config(arg)
            elif cmd == "shutdown":
Jelte Jansen's avatar
Jelte Jansen committed
329
                # TODO: logging
330 331 332 333 334 335 336 337 338 339
                print("[b10-cfgmgr] Received shutdown command")
                self.running = False
                answer = isc.config.ccsession.create_answer(0)
            elif cmd == isc.config.ccsession.COMMAND_MODULE_SPEC:
                try:
                    answer = self._handle_module_spec(isc.config.ModuleSpec(arg))
                except isc.config.ModuleSpecError as dde:
                    answer = isc.config.ccsession.create_answer(1, "Error in data definition: " + str(dde))
            else:
                answer = isc.config.ccsession.create_answer(1, "Unknown command: " + str(cmd))
340
        else:
341
            answer = isc.config.ccsession.create_answer(1, "Unknown message format: " + str(msg))
342 343 344
        return answer
        
    def run(self):
345
        """Runs the configuration manager."""
346 347 348
        self.running = True
        while (self.running):
            msg, env = self.cc.group_recvmsg(False)
349
            if msg and not 'result' in msg:
350 351 352 353
                answer = self.handle_msg(msg);
                self.cc.group_reply(env, answer)
            else:
                self.running = False