zonemgr.py.in 27.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!@PYTHON@

# 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.

18
"""
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
This file implements the Secondary Manager program.

The secondary manager is one of the co-operating processes
of BIND10, which keeps track of timers and other information
necessary for BIND10 to act as a slave.
"""

import sys; sys.path.append ('@@PYTHONPATH@@')
import os
import time
import signal
import isc
import random
import threading
import select
import socket
35
import errno
36
37
38
from isc.datasrc import sqlite3_ds
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
39
import isc.util.process
Michal Vaner's avatar
Michal Vaner committed
40

41
42
43
44
# Initialize logging for called modules.
# TODO: Log messages properly
isc.log.init("b10-zonemgr")

45
isc.util.process.rename()
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# If B10_FROM_BUILD is set in the environment, we use data files
# from a directory relative to that, otherwise we use the ones
# installed on the system
if "B10_FROM_BUILD" in os.environ:
    SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/zonemgr"
    AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
else:
    PREFIX = "@prefix@"
    DATAROOTDIR = "@datarootdir@"
    SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
    AUTH_SPECFILE_PATH = SPECFILE_PATH

SPECFILE_LOCATION = SPECFILE_PATH + "/zonemgr.spec"
AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"

__version__ = "BIND10"

64
# define module name
Jerry's avatar
Jerry committed
65
66
XFRIN_MODULE_NAME = 'Xfrin'
AUTH_MODULE_NAME = 'Auth'
67

68
# define command name
Jerry's avatar
Jerry committed
69
70
ZONE_XFRIN_FAILED_COMMAND = 'zone_xfrin_failed'
ZONE_XFRIN_SUCCESS_COMMAND = 'zone_new_data_ready'
71
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
Jerry's avatar
Jerry committed
72
ZONE_NOTIFY_COMMAND = 'notify'
73

74
75
76
77
78
# define zone state
ZONE_OK = 0
ZONE_REFRESHING = 1
ZONE_EXPIRED = 2

79
80
81
82
83
84
85
86
87
88
89
90
# offsets of fields in the SOA RDATA
REFRESH_OFFSET = 3
RETRY_OFFSET = 4
EXPIRED_OFFSET = 5

# verbose mode
VERBOSE_MODE = False

def log_msg(msg):
    if VERBOSE_MODE:
        sys.stdout.write("[b10-zonemgr] %s\n" % str(msg))

91
92
93
class ZonemgrException(Exception):
    pass

94
95
class ZonemgrRefresh:
    """This class will maintain and manage zone refresh info.
96
97
98
    It also provides methods to keep track of zone timers and
    do zone refresh.
    Zone timers can be started by calling run_timer(), and it
Jerry's avatar
Jerry committed
99
100
    can be stopped by calling shutdown() in another thread.

101
    """
102

103
    def __init__(self, cc, db_file, slave_socket, config_data):
104
        self._cc = cc
105
        self._check_sock = slave_socket
106
        self._db_file = db_file
107
        self._zonemgr_refresh_info = {}
108
109
110
111
112
        self._lowerbound_refresh = None
        self._lowerbound_retry = None
        self._max_transfer_timeout = None
        self._refresh_jitter = None
        self._reload_jitter = None
113
        self.update_config_data(config_data)
114
        self._running = False
115

116
117
118
    def _random_jitter(self, max, jitter):
        """Imposes some random jitters for refresh and
        retry timers to avoid many zones need to do refresh
119
        at the same time.
120
121
        The value should be between (max - jitter) and max.
        """
122
123
        if 0 == jitter:
            return max
124
        return random.uniform(max - jitter, max)
125
126
127
128

    def _get_current_time(self):
        return time.time()

Jerry's avatar
Jerry committed
129
    def _set_zone_timer(self, zone_name_class, max, jitter):
130
        """Set zone next refresh time.
131
        jitter should not be bigger than half the original value."""
Jerry's avatar
Jerry committed
132
        self._set_zone_next_refresh_time(zone_name_class, self._get_current_time() + \
133
                                            self._random_jitter(max, jitter))
134

Jerry's avatar
Jerry committed
135
    def _set_zone_refresh_timer(self, zone_name_class):
136
        """Set zone next refresh time after zone refresh success.
137
           now + refresh - refresh_jitter <= next_refresh_time <= now + refresh
138
           """
139
        zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[REFRESH_OFFSET])
140
        zone_refresh_time = max(self._lowerbound_refresh, zone_refresh_time)
141
        self._set_zone_timer(zone_name_class, zone_refresh_time, self._refresh_jitter * zone_refresh_time)
142

Jerry's avatar
Jerry committed
143
    def _set_zone_retry_timer(self, zone_name_class):
144
        """Set zone next refresh time after zone refresh fail.
145
           now + retry - retry_jitter <= next_refresh_time <= now + retry
146
           """
147
        zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[RETRY_OFFSET])
148
        zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
149
        self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
150

Jerry's avatar
Jerry committed
151
    def _set_zone_notify_timer(self, zone_name_class):
152
        """Set zone next refresh time after receiving notify
153
           next_refresh_time = now
154
        """
Jerry's avatar
Jerry committed
155
        self._set_zone_timer(zone_name_class, 0, 0)
156

Jerry's avatar
Jerry committed
157
    def _zone_not_exist(self, zone_name_class):
158
        """ Zone doesn't belong to zonemgr"""
159
        return not zone_name_class in self._zonemgr_refresh_info
Jerry's avatar
Jerry committed
160
161

    def zone_refresh_success(self, zone_name_class):
162
        """Update zone info after zone refresh success"""
Jerry's avatar
Jerry committed
163
        if (self._zone_not_exist(zone_name_class)):
164
165
            raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
166
        self.zonemgr_reload_zone(zone_name_class)
Jerry's avatar
Jerry committed
167
168
169
        self._set_zone_refresh_timer(zone_name_class)
        self._set_zone_state(zone_name_class, ZONE_OK)
        self._set_zone_last_refresh_time(zone_name_class, self._get_current_time())
170

Jerry's avatar
Jerry committed
171
    def zone_refresh_fail(self, zone_name_class):
172
        """Update zone info after zone refresh fail"""
Jerry's avatar
Jerry committed
173
        if (self._zone_not_exist(zone_name_class)):
174
175
            raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
176
177
178
179
180
        # Is zone expired?
        if (self._zone_is_expired(zone_name_class)):
            self._set_zone_state(zone_name_class, ZONE_EXPIRED)
        else:
            self._set_zone_state(zone_name_class, ZONE_OK)
Jerry's avatar
Jerry committed
181
        self._set_zone_retry_timer(zone_name_class)
182

Jerry's avatar
Jerry committed
183
    def zone_handle_notify(self, zone_name_class, master):
184
        """Handle zone notify"""
Jerry's avatar
Jerry committed
185
        if (self._zone_not_exist(zone_name_class)):
186
187
            raise ZonemgrException("[b10-zonemgr] Notified zone (%s, %s) "
                                   "doesn't belong to zonemgr" % zone_name_class)
Jerry's avatar
Jerry committed
188
189
        self._set_zone_notifier_master(zone_name_class, master)
        self._set_zone_notify_timer(zone_name_class)
190

191
192
193
194
195
196
197
    def zonemgr_reload_zone(self, zone_name_class):
        """ Reload a zone."""
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
        self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = zone_soa[7]

    def zonemgr_add_zone(self, zone_name_class):
        """ Add a zone into zone manager."""
198
        log_msg("Loading zone (%s, %s)" % zone_name_class)
199
200
201
202
203
204
        zone_info = {}
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
        if not zone_soa:
            raise ZonemgrException("[b10-zonemgr] zone (%s, %s) doesn't have soa." % zone_name_class)
        zone_info["zone_soa_rdata"] = zone_soa[7]
        zone_info["zone_state"] = ZONE_OK
205
        zone_info["last_refresh_time"] = self._get_current_time()
206
        self._zonemgr_refresh_info[zone_name_class] = zone_info
207
208
209
210
        # Imposes some random jitters to avoid many zones need to do refresh at the same time.
        zone_reload_jitter = float(zone_soa[7].split(" ")[RETRY_OFFSET])
        zone_reload_jitter = max(self._lowerbound_retry, zone_reload_jitter)
        self._set_zone_timer(zone_name_class, zone_reload_jitter, self._reload_jitter * zone_reload_jitter)
211

Jerry's avatar
Jerry committed
212
    def _zone_is_expired(self, zone_name_class):
213
214
        """Judge whether a zone is expired or not."""
        zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[EXPIRED_OFFSET])
Jerry's avatar
Jerry committed
215
216
        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
217
218
219
220
221
            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
            return True

        return False

Jerry's avatar
Jerry committed
222
223
    def _get_zone_soa_rdata(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]
224

Jerry's avatar
Jerry committed
225
226
    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
227

Jerry's avatar
Jerry committed
228
229
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
230

Jerry's avatar
Jerry committed
231
232
    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
233
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
234
235
236

        return None

Jerry's avatar
Jerry committed
237
238
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
239

Jerry's avatar
Jerry committed
240
241
242
    def _clear_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
            del self._zonemgr_refresh_info[zone_name_class]["notify_master"]
243

Jerry's avatar
Jerry committed
244
245
    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
246

Jerry's avatar
Jerry committed
247
    def _set_zone_state(self, zone_name_class, zone_state):
248
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
249

Jerry's avatar
Jerry committed
250
251
    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
252

Jerry's avatar
Jerry committed
253
254
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
255

Jerry's avatar
Jerry committed
256
257
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
258

Jerry's avatar
Jerry committed
259
260
    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
261
262

    def _send_command(self, module_name, command_name, params):
263
        """Send command between modules."""
264
        msg = create_command(command_name, params)
265
        try:
266
267
268
269
270
            seq = self._cc.group_sendmsg(msg, module_name)
            try:
                answer, env = self._cc.group_recvmsg(False, seq)
            except isc.cc.session.SessionTimeout:
                pass        # for now we just ignore the failure
271
        except socket.error:
272
            sys.stderr.write("[b10-zonemgr] Failed to send to module %s, the session has been closed." % module_name)
273

Jerry's avatar
Jerry committed
274
275
276
277
278
279
280
    def _find_need_do_refresh_zone(self):
        """Find the first zone need do refresh, if no zone need
        do refresh, return the zone with minimum next_refresh_time.
        """
        zone_need_refresh = None
        for zone_name_class in self._zonemgr_refresh_info.keys():
            zone_state = self._get_zone_state(zone_name_class)
281
            # If hasn't received refresh response but are within refresh timeout, skip the zone
282
            if (ZONE_REFRESHING == zone_state and
Jerry's avatar
Jerry committed
283
                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
284
                continue
285
286
287
288

            # Get the zone with minimum next_refresh_time
            if ((zone_need_refresh is None) or
                (self._get_zone_next_refresh_time(zone_name_class) <
289
                 self._get_zone_next_refresh_time(zone_need_refresh))):
Jerry's avatar
Jerry committed
290
291
                zone_need_refresh = zone_name_class

292
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
293
294
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
295

296
297
        return zone_need_refresh

298

Jerry's avatar
Jerry committed
299
    def _do_refresh(self, zone_name_class):
300
        """Do zone refresh."""
301
        log_msg("Do refresh for zone (%s, %s)." % zone_name_class)
Jerry's avatar
Jerry committed
302
        self._set_zone_state(zone_name_class, ZONE_REFRESHING)
303
        self._set_zone_refresh_timeout(zone_name_class, self._get_current_time() + self._max_transfer_timeout)
Jerry's avatar
Jerry committed
304
        notify_master = self._get_zone_notifier_master(zone_name_class)
305
        # If the zone has notify master, send notify command to xfrin module
306
        if notify_master:
Jerry's avatar
Jerry committed
307
308
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1],
309
                     "master" : notify_master
310
                     }
311
            self._send_command(XFRIN_MODULE_NAME, ZONE_NOTIFY_COMMAND, param)
Jerry's avatar
Jerry committed
312
            self._clear_zone_notifier_master(zone_name_class)
313
314
        # Send refresh command to xfrin module
        else:
Jerry's avatar
Jerry committed
315
316
317
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1]
                    }
Jerry's avatar
Jerry committed
318
            self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)
319
320
321

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
322
        if not len(self._zonemgr_refresh_info):
323
324
325
326
            return True

        return False

Michal Vaner's avatar
Michal Vaner committed
327
    def _run_timer(self, start_event):
328
        while self._running:
Michal Vaner's avatar
Michal Vaner committed
329
330
            # Notify run_timer that we already started and are inside the loop.
            # It is set only once, but when it was outside the loop, there was
Michal Vaner's avatar
Michal Vaner committed
331
            # a race condition and _running could be set to false before we
Michal Vaner's avatar
Michal Vaner committed
332
333
334
335
            # could enter it
            if start_event:
                start_event.set()
                start_event = None
Jerry's avatar
Jerry committed
336
            # If zonemgr has no zone, set timer timeout to self._lowerbound_retry.
337
            if self._zone_mgr_is_empty():
338
                timeout = self._lowerbound_retry
339
            else:
340
                zone_need_refresh = self._find_need_do_refresh_zone()
341
                # If don't get zone with minimum next refresh time, set timer timeout to self._lowerbound_retry.
342
                if not zone_need_refresh:
343
                    timeout = self._lowerbound_retry
344
345
346
347
348
                else:
                    timeout = self._get_zone_next_refresh_time(zone_need_refresh) - self._get_current_time()
                    if (timeout < 0):
                        self._do_refresh(zone_need_refresh)
                        continue
349

350
            """ Wait for the socket notification for a maximum time of timeout
351
            in seconds (as float)."""
352
            try:
Jerry's avatar
Jerry committed
353
                rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
354
355
356
357
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                else:
Jerry's avatar
Jerry committed
358
                    sys.stderr.write("[b10-zonemgr] Error with select(); %s\n" % e)
359
360
                    break

Jerry's avatar
Jerry committed
361
            for fd in rlist:
362
                if fd == self._read_sock: # awaken by shutdown socket
363
                    # self._running will be False by now, if it is not a false
Michal Vaner's avatar
Michal Vaner committed
364
365
                    # alarm (linux kernel is said to trigger spurious wakeup
                    # on a filehandle that is not really readable).
366
                    continue
Jerry's avatar
Jerry committed
367
368
                if fd == self._check_sock: # awaken by check socket
                    self._check_sock.recv(32)
Jerry's avatar
Jerry committed
369

370
371
372
373
374
375
376
377
378
379
380
381
382
    def run_timer(self, daemon=False):
        """
        Keep track of zone timers. Spawns and starts a thread. The thread object is returned.

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
            raise RuntimeError("Trying to run the timers twice at the same time")

        # Prepare the launch
        self._running = True
        (self._read_sock, self._write_sock) = socket.socketpair()
Michal Vaner's avatar
Michal Vaner committed
383
        start_event = threading.Event()
384
385

        # Start the thread
Michal Vaner's avatar
Michal Vaner committed
386
387
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
388
389
390
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
Michal Vaner's avatar
Michal Vaner committed
391
        start_event.wait()
392
393
394
395

        # Return the thread to anyone interested
        return self._thread

Jerry's avatar
Jerry committed
396
    def shutdown(self):
397
398
399
400
401
402
403
404
405
        """
        Stop the run_timer() thread. Block until it finished. This must be
        called from a different thread.
        """
        if not self._running:
            raise RuntimeError("Trying to shutdown, but not running")

        # Ask the thread to stop
        self._running = False
Jerry's avatar
Jerry committed
406
        self._write_sock.send(b'shutdown') # make self._read_sock readble
407
408
409
410
411
412
        # Wait for it to actually finnish
        self._thread.join()
        # Wipe out what we do not need
        self._thread = None
        self._read_sock = None
        self._write_sock = None
413

414
415
    def update_config_data(self, new_config):
        """ update ZonemgrRefresh config """
416
417
418
419
        # TODO: we probably want to store all this info in a nice
        # class, so that we don't have to backup and restore every
        # single value.
        # TODO2: We also don't use get_default_value yet
420
        backup = self._zonemgr_refresh_info.copy()
421

422
        # Get a new value, but only if it is defined (commonly used below)
423
424
425
426
427
428
429
430
        # We don't use "value or default", because if value would be
        # 0, we would take default
        def val_or_default(value, default):
            if value is not None:
                return value
            else:
                return default

431
432
        # store the values so we can restore them if there is a problem
        lowerbound_refresh_backup = self._lowerbound_refresh
433
434
        self._lowerbound_refresh = val_or_default(
            new_config.get('lowerbound_refresh'), self._lowerbound_refresh)
435
436

        lowerbound_retry_backup = self._lowerbound_retry
437
438
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
439
440

        max_transfer_timeout_backup = self._max_transfer_timeout
441
442
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
443
444

        refresh_jitter_backup = self._refresh_jitter
445
446
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
447
448

        reload_jitter_backup = self._reload_jitter
449
450
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
451
452
        try:
            required = {}
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
            secondary_zones = new_config.get('secondary_zones')
            if secondary_zones is not None:
                # Add new zones
                for secondary_zone in new_config.get('secondary_zones'):
                    name = secondary_zone['name']
                    # Be tolerant to sclerotic users who forget the final dot
                    if name[-1] != '.':
                        name = name + '.'
                    name_class = (name, secondary_zone['class'])
                    required[name_class] = True
                    # Add it only if it isn't there already
                    if not name_class in self._zonemgr_refresh_info:
                        self.zonemgr_add_zone(name_class)
                # Drop the zones that are no longer there
                # Do it in two phases, python doesn't like deleting while iterating
                to_drop = []
                for old_zone in self._zonemgr_refresh_info:
                    if not old_zone in required:
                        to_drop.append(old_zone)
                for drop in to_drop:
                    del self._zonemgr_refresh_info[drop]
474
475
476
        # If we are not able to find it in database, restore the original
        except:
            self._zonemgr_refresh_info = backup
477
478
479
480
481
            self._lowerbound_refresh = lowerbound_refresh_backup
            self._lowerbound_retry = lowerbound_retry_backup
            self._max_transfer_timeout = max_transfer_timeout_backup
            self._refresh_jitter = refresh_jitter_backup
            self._reload_jitter = reload_jitter_backup
482
            raise
483

484
485
486
class Zonemgr:
    """Zone manager class."""
    def __init__(self):
487
        self._zone_refresh = None
488
489
        self._setup_session()
        self._db_file = self.get_db_file()
490
        # Create socket pair for communicating between main thread and zonemgr timer thread
Jerry's avatar
Jerry committed
491
        self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
492
        self._zone_refresh = ZonemgrRefresh(self._cc, self._db_file, self._slave_socket, self._config_data)
493
        self._zone_refresh.run_timer()
494

495
        self._lock = threading.Lock()
496
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
497
        self.running = False
498
499

    def _setup_session(self):
500
        """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
501
502
        commands and config data sent from other modules, another one (self._cc)
        is used to send commands to proper modules."""
503
504
505
506
        self._cc = isc.cc.Session()
        self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                                  self.config_handler,
                                                  self.command_handler)
507
        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
508
        self._config_data = self._module_cc.get_full_config()
Jerry's avatar
Jerry committed
509
        self._config_data_check(self._config_data)
510
511
512
        self._module_cc.start()

    def get_db_file(self):
Jerry's avatar
Jerry committed
513
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
514
515
516
        # this too should be unnecessary, but currently the
        # 'from build' override isn't stored in the config
        # (and we don't have indirect python access to datasources yet)
517
518
519
520
521
        if is_default and "B10_FROM_BUILD" in os.environ:
            db_file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
        return db_file

    def shutdown(self):
522
523
        """Shutdown the zonemgr process. the thread which is keeping track of zone
        timers should be terminated.
524
        """
Jerry's avatar
Jerry committed
525
526
        self._zone_refresh.shutdown()

Jerry's avatar
Jerry committed
527
        self._slave_socket.close()
Jerry's avatar
Jerry committed
528
        self._master_socket.close()
529
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
530
        self.running = False
531
532

    def config_handler(self, new_config):
533
        """ Update config data. """
534
        answer = create_answer(0)
535
        ok = True
536
        complete = self._config_data.copy()
537
        for key in new_config:
538
            if key not in complete:
539
                answer = create_answer(1, "Unknown config data: " + str(key))
540
                ok = False
541
                continue
542
            complete[key] = new_config[key]
543

544
        self._config_data_check(complete)
545
        if self._zone_refresh is not None:
546
547
548
549
550
551
552
            try:
                self._zone_refresh.update_config_data(complete)
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete
553

554
555
        return answer

Jerry's avatar
Jerry committed
556
    def _config_data_check(self, config_data):
557
558
559
        """Check whether the new config data is valid or
        not. It contains only basic logic, not full check against
        database."""
Jerry's avatar
Jerry committed
560
        # jitter should not be bigger than half of the original value
561
562
563
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
            log_msg("[b10-zonemgr] refresh_jitter is too big, its value will "
564
                      "be set to 0.5")
Jerry's avatar
Jerry committed
565
566


Jerry's avatar
Jerry committed
567
    def _parse_cmd_params(self, args, command):
568
569
        zone_name = args.get("zone_name")
        if not zone_name:
570
            raise ZonemgrException("zone name should be provided")
571

Jerry's avatar
Jerry committed
572
573
        zone_class = args.get("zone_class")
        if not zone_class:
574
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
575
576
577
578

        if (command != ZONE_NOTIFY_COMMAND):
            return (zone_name, zone_class)

579
580
581
        master_str = args.get("master")
        if not master_str:
            raise ZonemgrException("master address should be provided")
582

Jerry's avatar
Jerry committed
583
        return ((zone_name, zone_class), master_str)
584
585
586


    def command_handler(self, command, args):
587
        """Handle command receivd from command channel.
588
        ZONE_NOTIFY_COMMAND is issued by Auth process; ZONE_XFRIN_SUCCESS_COMMAND
589
590
        and ZONE_XFRIN_FAILED_COMMAND are issued by Xfrin process; shutdown is issued
        by a user or Boss process. """
591
        answer = create_answer(0)
Jerry's avatar
Jerry committed
592
        if command == ZONE_NOTIFY_COMMAND:
Jerry's avatar
Jerry committed
593
            """ Handle Auth notify command"""
594
            # master is the source sender of the notify message.
Jerry's avatar
Jerry committed
595
            zone_name_class, master = self._parse_cmd_params(args, command)
596
            log_msg("Received notify command for zone (%s, %s)." % zone_name_class)
597
            with self._lock:
598
                self._zone_refresh.zone_handle_notify(zone_name_class, master)
599
            # Send notification to zonemgr timer thread
Jerry's avatar
Jerry committed
600
            self._master_socket.send(b" ")# make self._slave_socket readble
601

Jerry's avatar
Jerry committed
602
        elif command == ZONE_XFRIN_SUCCESS_COMMAND:
603
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
604
            zone_name_class = self._parse_cmd_params(args, command)
605
            with self._lock:
606
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jerry's avatar
Jerry committed
607
            self._master_socket.send(b" ")# make self._slave_socket readble
608

Jerry's avatar
Jerry committed
609
        elif command == ZONE_XFRIN_FAILED_COMMAND:
Jerry's avatar
Jerry committed
610
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
611
            zone_name_class = self._parse_cmd_params(args, command)
612
            with self._lock:
613
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jerry's avatar
Jerry committed
614
            self._master_socket.send(b" ")# make self._slave_socket readble
615
616
617
618
619
620
621
622
623
624

        elif command == "shutdown":
            self.shutdown()

        else:
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
Michal Vaner's avatar
Michal Vaner committed
625
        self.running = True
626
        while not self._shutdown_event.is_set():
627
            self._module_cc.check_command(False)
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654

zonemgrd = None

def signal_handler(signal, frame):
    if zonemgrd:
        zonemgrd.shutdown()
        sys.exit(0)

def set_signal_handler():
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)

def set_cmd_options(parser):
    parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
            help="display more about what is going on")

if '__main__' == __name__:
    try:
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
        VERBOSE_MODE = options.verbose

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
Jeremy C. Reed's avatar
Jeremy C. Reed committed
655
        sys.stderr.write("[b10-zonemgr] exit zonemgr process\n")
656
    except isc.cc.session.SessionError as e:
657
        sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
658
                           "is the command channel daemon running?\n")
659
    except isc.cc.session.SessionTimeout as e:
660
        sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
661
                           "is the configuration manager running?\n")
662
    except isc.config.ModuleCCSessionError as e:
Jeremy C. Reed's avatar
Jeremy C. Reed committed
663
        sys.stderr.write("[b10-zonemgr] exit zonemgr process: %s\n" % str(e))
664

Michal Vaner's avatar
Michal Vaner committed
665
    if zonemgrd and zonemgrd.running:
666
667
        zonemgrd.shutdown()