zonemgr.py.in 28.1 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
40
from zonemgr_messages import *
Michal Vaner's avatar
Michal Vaner committed
41

42
43
# Initialize logging for called modules.
isc.log.init("b10-zonemgr")
44
45
46
47
48
49
logger = isc.log.Logger("zonemgr")

# Constants for debug levels, to be removed when we have #1074.
DBG_START_SHUT = 0
DBG_ZONEMGR_COMMAND = 10
DBG_ZONEMGR_BASIC = 40
50

51
isc.util.process.rename()
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

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

70
# define module name
Jerry's avatar
Jerry committed
71
72
XFRIN_MODULE_NAME = 'Xfrin'
AUTH_MODULE_NAME = 'Auth'
73

74
# define command name
Jerry's avatar
Jerry committed
75
76
ZONE_XFRIN_FAILED_COMMAND = 'zone_xfrin_failed'
ZONE_XFRIN_SUCCESS_COMMAND = 'zone_new_data_ready'
77
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
Jerry's avatar
Jerry committed
78
ZONE_NOTIFY_COMMAND = 'notify'
79

80
81
82
83
84
# define zone state
ZONE_OK = 0
ZONE_REFRESHING = 1
ZONE_EXPIRED = 2

85
86
87
88
89
# offsets of fields in the SOA RDATA
REFRESH_OFFSET = 3
RETRY_OFFSET = 4
EXPIRED_OFFSET = 5

90
91
92
class ZonemgrException(Exception):
    pass

93
94
class ZonemgrRefresh:
    """This class will maintain and manage zone refresh info.
95
96
97
    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
98
    can be stopped by calling shutdown() in another thread.
99
    """
100

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

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

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

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

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

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

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

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

    def zone_refresh_success(self, zone_name_class):
160
        """Update zone info after zone refresh success"""
Jerry's avatar
Jerry committed
161
        if (self._zone_not_exist(zone_name_class)):
162
            logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0], zone_name_class[1])
163
164
            raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
165
        self.zonemgr_reload_zone(zone_name_class)
Jerry's avatar
Jerry committed
166
167
168
        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())
169

Jerry's avatar
Jerry committed
170
    def zone_refresh_fail(self, zone_name_class):
171
        """Update zone info after zone refresh fail"""
Jerry's avatar
Jerry committed
172
        if (self._zone_not_exist(zone_name_class)):
173
            logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0], zone_name_class[1])
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
            logger.error(ZONEMGR_UNKNOWN_ZONE_NOTIFIED, zone_name_class[0], zone_name_class[1])
187
188
            raise ZonemgrException("[b10-zonemgr] Notified zone (%s, %s) "
                                   "doesn't belong to zonemgr" % zone_name_class)
Jerry's avatar
Jerry committed
189
190
        self._set_zone_notifier_master(zone_name_class, master)
        self._set_zone_notify_timer(zone_name_class)
191

192
193
194
195
196
197
198
    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."""
199
200

        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
201
202
203
        zone_info = {}
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
        if not zone_soa:
204
            logger.error(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
205
206
207
            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
208
        zone_info["last_refresh_time"] = self._get_current_time()
209
        self._zonemgr_refresh_info[zone_name_class] = zone_info
210
211
212
213
        # 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)
214

Jerry's avatar
Jerry committed
215
    def _zone_is_expired(self, zone_name_class):
216
217
        """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
218
219
        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
220
221
222
223
224
            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
            return True

        return False

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

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

Jerry's avatar
Jerry committed
231
232
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
233

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

        return None

Jerry's avatar
Jerry committed
240
241
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
242

Jerry's avatar
Jerry committed
243
244
245
    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"]
246

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

Jerry's avatar
Jerry committed
250
    def _set_zone_state(self, zone_name_class, zone_state):
251
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
252

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

Jerry's avatar
Jerry committed
256
257
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
258

Jerry's avatar
Jerry committed
259
260
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
261

Jerry's avatar
Jerry committed
262
263
    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
264
265

    def _send_command(self, module_name, command_name, params):
266
        """Send command between modules."""
267
        msg = create_command(command_name, params)
268
        try:
269
270
271
272
273
            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
274
        except socket.error:
275
            logger.error(ZONEMGR_SEND_FAIL, module_name)
276

Jerry's avatar
Jerry committed
277
278
279
280
281
282
283
    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)
284
285
            # If hasn't received refresh response but are within refresh
            # timeout, skip the zone
286
            if (ZONE_REFRESHING == zone_state and
Jerry's avatar
Jerry committed
287
                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
288
                continue
289
290
291
292

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

296
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
297
298
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
299

300
301
        return zone_need_refresh

302

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

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
326
        if not len(self._zonemgr_refresh_info):
327
328
329
330
            return True

        return False

Michal Vaner's avatar
Michal Vaner committed
331
    def _run_timer(self, start_event):
332
        while self._running:
Michal Vaner's avatar
Michal Vaner committed
333
334
            # 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
335
            # a race condition and _running could be set to false before we
Michal Vaner's avatar
Michal Vaner committed
336
337
338
339
            # could enter it
            if start_event:
                start_event.set()
                start_event = None
Jerry's avatar
Jerry committed
340
            # If zonemgr has no zone, set timer timeout to self._lowerbound_retry.
341
            if self._zone_mgr_is_empty():
342
                timeout = self._lowerbound_retry
343
            else:
344
                zone_need_refresh = self._find_need_do_refresh_zone()
345
                # If don't get zone with minimum next refresh time, set timer timeout to self._lowerbound_retry.
346
                if not zone_need_refresh:
347
                    timeout = self._lowerbound_retry
348
349
350
351
352
                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
353

354
            """ Wait for the socket notification for a maximum time of timeout
355
            in seconds (as float)."""
356
            try:
Jerry's avatar
Jerry committed
357
                rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
358
359
360
361
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                else:
362
                    logger.error(ZONEMGR_SELECT_ERROR, e);
363
364
                    break

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

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

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
383
            logger.error(ZONEMGR_TIMER_THREAD_RUNNING)
384
385
386
387
388
            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
389
        start_event = threading.Event()
390
391

        # Start the thread
Michal Vaner's avatar
Michal Vaner committed
392
393
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
394
395
396
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
Michal Vaner's avatar
Michal Vaner committed
397
        start_event.wait()
398
399
400
401

        # Return the thread to anyone interested
        return self._thread

Jerry's avatar
Jerry committed
402
    def shutdown(self):
403
404
405
406
407
        """
        Stop the run_timer() thread. Block until it finished. This must be
        called from a different thread.
        """
        if not self._running:
408
            logger.error(ZONEMGR_NO_TIMER_THREAD)
409
410
411
412
            raise RuntimeError("Trying to shutdown, but not running")

        # Ask the thread to stop
        self._running = False
Jerry's avatar
Jerry committed
413
        self._write_sock.send(b'shutdown') # make self._read_sock readble
414
415
416
417
418
419
        # 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
420

421
422
    def update_config_data(self, new_config):
        """ update ZonemgrRefresh config """
423
424
425
426
        # 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
427
        backup = self._zonemgr_refresh_info.copy()
428

429
        # Get a new value, but only if it is defined (commonly used below)
430
431
432
433
434
435
436
437
        # 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

438
439
        # store the values so we can restore them if there is a problem
        lowerbound_refresh_backup = self._lowerbound_refresh
440
441
        self._lowerbound_refresh = val_or_default(
            new_config.get('lowerbound_refresh'), self._lowerbound_refresh)
442
443

        lowerbound_retry_backup = self._lowerbound_retry
444
445
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
446
447

        max_transfer_timeout_backup = self._max_transfer_timeout
448
449
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
450
451

        refresh_jitter_backup = self._refresh_jitter
452
453
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
454
455

        reload_jitter_backup = self._reload_jitter
456
457
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
458
459
        try:
            required = {}
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
            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]
481
482
483
        # If we are not able to find it in database, restore the original
        except:
            self._zonemgr_refresh_info = backup
484
485
486
487
488
            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
489
            raise
490

491
492
493
class Zonemgr:
    """Zone manager class."""
    def __init__(self):
494
        self._zone_refresh = None
495
496
        self._setup_session()
        self._db_file = self.get_db_file()
497
        # Create socket pair for communicating between main thread and zonemgr timer thread
Jerry's avatar
Jerry committed
498
        self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
499
        self._zone_refresh = ZonemgrRefresh(self._cc, self._db_file, self._slave_socket, self._config_data)
500
        self._zone_refresh.run_timer()
501

502
        self._lock = threading.Lock()
503
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
504
        self.running = False
505
506

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

    def get_db_file(self):
Jerry's avatar
Jerry committed
520
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
521
522
523
        # 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)
524
525
526
527
528
        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):
529
530
        """Shutdown the zonemgr process. The thread which is keeping track of
           zone timers should be terminated.
531
        """
Jerry's avatar
Jerry committed
532
533
        self._zone_refresh.shutdown()

Jerry's avatar
Jerry committed
534
        self._slave_socket.close()
Jerry's avatar
Jerry committed
535
        self._master_socket.close()
536
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
537
        self.running = False
538
539

    def config_handler(self, new_config):
540
        """ Update config data. """
541
        answer = create_answer(0)
542
        ok = True
543
        complete = self._config_data.copy()
544
        for key in new_config:
545
            if key not in complete:
546
                answer = create_answer(1, "Unknown config data: " + str(key))
547
                ok = False
548
                continue
549
            complete[key] = new_config[key]
550

551
        self._config_data_check(complete)
552
        if self._zone_refresh is not None:
553
554
555
556
557
558
559
            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
560

561
562
        return answer

Jerry's avatar
Jerry committed
563
    def _config_data_check(self, config_data):
564
565
566
        """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
567
        # jitter should not be bigger than half of the original value
568
569
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
570
            logger.warn(ZONEMGR_JITTER_TOO_BIG)
Jerry's avatar
Jerry committed
571

Jerry's avatar
Jerry committed
572
    def _parse_cmd_params(self, args, command):
573
574
        zone_name = args.get("zone_name")
        if not zone_name:
575
            logger.error(ZONEMGR_NO_ZONE_NAME)
576
            raise ZonemgrException("zone name should be provided")
577

Jerry's avatar
Jerry committed
578
579
        zone_class = args.get("zone_class")
        if not zone_class:
580
            logger.error(ZONEMGR_NO_ZONE_CLASS)
581
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
582
583
584
585

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

586
587
        master_str = args.get("master")
        if not master_str:
588
            logger.error(ZONEMGR_NO_MASTER_ADDRESS)
589
            raise ZonemgrException("master address should be provided")
590

Jerry's avatar
Jerry committed
591
        return ((zone_name, zone_class), master_str)
592
593
594


    def command_handler(self, command, args):
595
        """Handle command receivd from command channel.
596
597
598
599
        ZONE_NOTIFY_COMMAND is issued by Auth process;
        ZONE_XFRIN_SUCCESS_COMMAND and ZONE_XFRIN_FAILED_COMMAND are issued by
        Xfrin process;
        shutdown is issued by a user or Boss process. """
600
        answer = create_answer(0)
Jerry's avatar
Jerry committed
601
        if command == ZONE_NOTIFY_COMMAND:
Jerry's avatar
Jerry committed
602
            """ Handle Auth notify command"""
603
            # master is the source sender of the notify message.
Jerry's avatar
Jerry committed
604
            zone_name_class, master = self._parse_cmd_params(args, command)
605
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY, zone_name_class[0], zone_name_class[1])
606
            with self._lock:
607
                self._zone_refresh.zone_handle_notify(zone_name_class, master)
608
            # Send notification to zonemgr timer thread
Jerry's avatar
Jerry committed
609
            self._master_socket.send(b" ")# make self._slave_socket readble
610

Jerry's avatar
Jerry committed
611
        elif command == ZONE_XFRIN_SUCCESS_COMMAND:
612
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
613
            zone_name_class = self._parse_cmd_params(args, command)
614
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
615
            with self._lock:
616
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jerry's avatar
Jerry committed
617
            self._master_socket.send(b" ")# make self._slave_socket readble
618

Jerry's avatar
Jerry committed
619
        elif command == ZONE_XFRIN_FAILED_COMMAND:
Jerry's avatar
Jerry committed
620
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
621
            zone_name_class = self._parse_cmd_params(args, command)
622
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])
623
            with self._lock:
624
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jerry's avatar
Jerry committed
625
            self._master_socket.send(b" ")# make self._slave_socket readble
626
627

        elif command == "shutdown":
628
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
629
630
631
            self.shutdown()

        else:
632
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
633
634
635
636
637
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
Michal Vaner's avatar
Michal Vaner committed
638
        self.running = True
639
        while not self._shutdown_event.is_set():
640
            self._module_cc.check_command(False)
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658

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:
659
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
660
661
662
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
663
        if options.verbose:
664
            logger.set_severity("DEBUG", 99)
665
666
667
668
669

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
670
671
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

672
    except isc.cc.session.SessionError as e:
673
674
        logger.error(ZONEMGR_SESSION_ERROR)

675
    except isc.cc.session.SessionTimeout as e:
676
677
        logger.error(ZONEMGR_SESSION_TIMEOUT)

678
    except isc.config.ModuleCCSessionError as e:
679
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))
680

Michal Vaner's avatar
Michal Vaner committed
681
    if zonemgrd and zonemgrd.running:
682
683
        zonemgrd.shutdown()

684
    logger.debug(DBG_START_SHUT, ZONEMGR_SHUTDOWN)