cfgmgr.py 13.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# 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.

#
# This is the main class for the b10-cfgmgr daemon
#

20
import isc
21 22 23 24
import signal
import ast
import pprint
import os
25
from isc.cc import data
26

Jelte Jansen's avatar
Jelte Jansen committed
27 28 29 30 31 32
class ConfigManagerDataReadError(Exception):
    pass

class ConfigManagerDataEmpty(Exception):
    pass

33 34 35
class ConfigManagerData:
    CONFIG_VERSION = 1

Jelte Jansen's avatar
Jelte Jansen committed
36 37 38 39 40
    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."""
41 42
        self.data = {}
        self.data['version'] = ConfigManagerData.CONFIG_VERSION
Jelte Jansen's avatar
Jelte Jansen committed
43
        self.data_path = data_path
Jelte Jansen's avatar
Jelte Jansen committed
44
        self.db_filename = data_path + os.sep + file_name
45 46

    def set_data_definition(self, module_name, module_data_definition):
Jelte Jansen's avatar
Jelte Jansen committed
47 48
        """Set the data definition for the given module name."""
        #self.zones[module_name] = module_data_definition
49 50
        self.data_definitions[module_name] = module_data_definition

Jelte Jansen's avatar
Jelte Jansen committed
51 52 53 54 55 56 57 58 59 60
    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)
61
        try:
62
            file = open(config.db_filename, 'r')
63 64 65 66 67
            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
68 69
                # We can put in a migration path here for old data
                raise ConfigManagerDataReadError("[bind-cfgd] Old version of data found")
70 71
            file.close()
        except IOError as ioe:
Jelte Jansen's avatar
Jelte Jansen committed
72
            raise ConfigManagerDataEmpty("No config file found")
73
        except:
Jelte Jansen's avatar
Jelte Jansen committed
74
            raise ConfigManagerDataReadError("Config file unreadable")
75 76 77

        return config
        
Jelte Jansen's avatar
Jelte Jansen committed
78 79 80 81
    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."""
82
        try:
Jelte Jansen's avatar
Jelte Jansen committed
83
            tmp_filename = self.db_filename + ".tmp"
84 85 86 87 88 89
            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
90
            os.rename(tmp_filename, self.db_filename)
91 92 93
        except IOError as ioe:
            print("Unable to write config file; configuration not stored")

Jelte Jansen's avatar
Jelte Jansen committed
94 95 96 97 98 99 100
    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

101
class ConfigManager:
Jelte Jansen's avatar
Jelte Jansen committed
102 103 104 105 106 107 108
    """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):
109 110
        # remove these and use self.data_specs
        #self.commands = {}
111
        self.data_definitions = {}
112

Jelte Jansen's avatar
Jelte Jansen committed
113
        self.data_path = data_path
114
        self.data_specs = {}
Jelte Jansen's avatar
Jelte Jansen committed
115
        self.config = ConfigManagerData(data_path)
Jelte Jansen's avatar
Jelte Jansen committed
116 117 118 119
        if session:
            self.cc = session
        else:
            self.cc = isc.cc.Session()
120 121 122 123 124
        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
125
        """Notifies the Boss module that the Config Manager is running"""
126 127
        self.cc.group_sendmsg({"running": "configmanager"}, "Boss")

128 129 130
    def set_data_spec(self, spec):
        #data_def = isc.config.DataDefinition(spec)
        self.data_specs[spec.get_module_name()] = spec
131

132 133 134
    def get_data_spec(self, module_name):
        if module_name in self.data_specs:
            return self.data_specs[module_name]
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
    def get_config_data(self, name = None):
        """Returns a dict containing 'module_name': config_data for
           all modules. If name is specified, only that module will
           be included"""
        config_data = {}
        if name:
            if name in self.data_specs:
                config_data[name] = self.data_specs[name].get_data
        else:
            for module_name in self.data_specs.keys():
                config_data[module_name] = self.data_specs[module_name].get_config_data()
        return config_data

    def get_commands(self, name = None):
        """Returns a dict containing 'module_name': commands_dict for
           all modules. If name is specified, only that module will
           be included"""
        commands = {}
        if name:
            if name in self.data_specs:
                commands[name] = self.data_specs[name].get_commands
        else:
            for module_name in self.data_specs.keys():
                commands[module_name] = self.data_specs[module_name].get_commands()
        return commands
161 162

    def read_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
163 164
        """Read the current configuration from the b10-config.db file
           at the path specificied at init()"""
165 166 167 168 169
        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)
170 171
        
    def write_config(self):
Jelte Jansen's avatar
Jelte Jansen committed
172 173
        """Write the current configuration to the b10-config.db file
           at the path specificied at init()"""
174 175
        self.config.write_to_file()

Jelte Jansen's avatar
Jelte Jansen committed
176 177 178 179 180 181
    def _handle_get_data_spec(self, cmd):
        answer = {}
        if len(cmd) > 1:
            if type(cmd[1]) == dict:
                if 'module_name' in cmd[1] and cmd[1]['module_name'] != '':
                    module_name = cmd[1]['module_name']
182
                    answer["result"] = [0, self.get_config_data(module_name)]
Jelte Jansen's avatar
Jelte Jansen committed
183 184 185 186 187
                else:
                    answer["result"] = [1, "Bad module_name in get_data_spec command"]
            else:
                answer["result"] = [1, "Bad get_data_spec command, argument not a dict"]
        else:
188
            answer["result"] = [0, self.get_config_data()]
Jelte Jansen's avatar
Jelte Jansen committed
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
        return answer

    def _handle_get_config(self, cmd):
        answer = {}
        if len(cmd) > 1:
            if type(cmd[1]) == dict:
                if 'module_name' in cmd[1] and cmd[1]['module_name'] != '':
                    module_name = cmd[1]['module_name']
                    try:
                        answer["result"] = [0, data.find(self.config.data, module_name) ]
                    except data.DataNotFoundError as dnfe:
                        # no data is ok, that means we have nothing that
                        # deviates from default values
                        answer["result"] = [0, {} ]
                else:
                    answer["result"] = [1, "Bad module_name in get_config command"]
            else:
                answer["result"] = [1, "Bad get_config command, argument not a dict"]
        else:
            answer["result"] = [0, self.config.data]
        return answer

    def _handle_set_config(self, cmd):
        answer = {}
        if len(cmd) == 3:
            # todo: use api (and check the data against the definition?)
            module_name = cmd[1]
            conf_part = data.find_no_exc(self.config.data, module_name)
            if conf_part:
                data.merge(conf_part, cmd[2])
                self.cc.group_sendmsg({ "config_update": conf_part }, module_name)
220
                answer, env = self.cc.group_recvmsg(False)
Jelte Jansen's avatar
Jelte Jansen committed
221 222 223 224 225
            else:
                conf_part = data.set(self.config.data, module_name, {})
                data.merge(conf_part[module_name], cmd[2])
                # send out changed info
                self.cc.group_sendmsg({ "config_update": conf_part[module_name] }, module_name)
226 227 228 229 230 231
                # replace 'our' answer with that of the module
                answer, env = selc.cc.group_recvmsg(False)
                print("[XX] module responded with")
                print(answer)
            if answer and "result" in answer and answer['result'][0] == 0:
                self.write_config()
Jelte Jansen's avatar
Jelte Jansen committed
232 233 234 235
        elif len(cmd) == 2:
            # todo: use api (and check the data against the definition?)
            data.merge(self.config.data, cmd[1])
            # send out changed info
236
            got_error = False
Jelte Jansen's avatar
Jelte Jansen committed
237 238 239
            for module in self.config.data:
                if module != "version":
                    self.cc.group_sendmsg({ "config_update": self.config.data[module] }, module)
240 241 242 243 244 245 246 247
                    answer, env = self.cc.group_recvmsg(False)
                    print("[XX] one module responded with")
                    print(answer)
                    if answer and 'result' in answer and answer['result'][0] != 0:
                        got_error = True
            if not got_error:
                self.write_config()
            # TODO rollback changes that did get through?
Jelte Jansen's avatar
Jelte Jansen committed
248 249 250 251 252 253 254 255 256
        else:
            answer["result"] = [ 1, "Wrong number of arguments" ]
        return answer

    def _handle_data_specification(self, spec):
        # todo: validate? (no direct access to spec as
        # todo: use DataDefinition class
        # todo: error checking (like keyerrors)
        answer = {}
257
        self.set_data_spec(spec)
258 259
        print("[XX] cfgmgr add spec:")
        print(spec)
260 261 262 263 264
        
        # We should make one general 'spec update for module' that
        # passes both specification and commands at once
        self.cc.group_sendmsg({ "specification_update": [ spec.get_module_name(), spec.get_config_data() ] }, "Cmd-Ctrld")
        self.cc.group_sendmsg({ "commands_update": [ spec.get_module_name(), spec.get_commands() ] }, "Cmd-Ctrld")
Jelte Jansen's avatar
Jelte Jansen committed
265 266 267
        answer["result"] = [ 0 ]
        return answer

268
    def handle_msg(self, msg):
Jelte Jansen's avatar
Jelte Jansen committed
269
        """Handle a direct command"""
270 271 272 273 274
        answer = {}
        if "command" in msg:
            cmd = msg["command"]
            try:
                if cmd[0] == "get_commands":
275
                    answer["result"] = [ 0, self.get_commands() ]
276
                elif cmd[0] == "get_data_spec":
Jelte Jansen's avatar
Jelte Jansen committed
277
                    answer = self._handle_get_data_spec(cmd)
278
                elif cmd[0] == "get_config":
Jelte Jansen's avatar
Jelte Jansen committed
279
                    answer = self._handle_get_config(cmd)
280
                elif cmd[0] == "set_config":
Jelte Jansen's avatar
Jelte Jansen committed
281
                    answer = self._handle_set_config(cmd)
282 283 284
                elif cmd[0] == "shutdown":
                    print("[bind-cfgd] Received shutdown command")
                    self.running = False
Jelte Jansen's avatar
Jelte Jansen committed
285
                    answer["result"] = [ 0 ]
286 287 288 289 290 291
                else:
                    answer["result"] = [ 1, "Unknown command: " + str(cmd) ]
            except IndexError as ie:
                answer["result"] = [ 1, "Missing argument in command: " + str(ie) ]
                raise ie
        elif "data_specification" in msg:
292 293 294 295
            try:
                answer = self._handle_data_specification(isc.config.DataDefinition(msg["data_specification"]))
            except isc.config.DataDefinitionError as dde:
                answer['result'] = [ 1, "Error in data definition: " + str(dde) ]
296
        elif 'result' in msg:
Jelte Jansen's avatar
Jelte Jansen committed
297
            # this seems wrong, might start pingpong
298 299
            answer['result'] = [0]
        else:
Jelte Jansen's avatar
Jelte Jansen committed
300
            answer["result"] = [ 1, "Unknown message format: " + str(msg) ]
301 302 303 304 305 306 307 308
        return answer
        
    def run(self):
        self.running = True
        while (self.running):
            msg, env = self.cc.group_recvmsg(False)
            if msg:
                answer = self.handle_msg(msg);
309 310
                print("[XX] CFGMGR Sending answer to UI:")
                print(answer)
311 312 313 314 315 316 317 318 319 320
                self.cc.group_reply(env, answer)
            else:
                self.running = False

cm = None

def signal_handler(signal, frame):
    global cm
    if cm:
        cm.running = False