zonemgr.py.in 27.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
Michal Vaner's avatar
Michal Vaner committed
40

41
isc.util.process.rename()
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

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

60
# define module name
Jerry's avatar
Jerry committed
61
62
XFRIN_MODULE_NAME = 'Xfrin'
AUTH_MODULE_NAME = 'Auth'
63

64
# define command name
Jerry's avatar
Jerry committed
65
66
ZONE_XFRIN_FAILED_COMMAND = 'zone_xfrin_failed'
ZONE_XFRIN_SUCCESS_COMMAND = 'zone_new_data_ready'
67
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
Jerry's avatar
Jerry committed
68
ZONE_NOTIFY_COMMAND = 'notify'
69

70
71
72
73
74
# define zone state
ZONE_OK = 0
ZONE_REFRESHING = 1
ZONE_EXPIRED = 2

75
76
77
78
79
80
81
82
83
84
85
86
# 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))

87
88
89
class ZonemgrException(Exception):
    pass

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

97
    """
98

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

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

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

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

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

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

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

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

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

Jerry's avatar
Jerry committed
167
    def zone_refresh_fail(self, zone_name_class):
168
        """Update zone info after zone refresh fail"""
Jerry's avatar
Jerry committed
169
        if (self._zone_not_exist(zone_name_class)):
170
171
            raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
172
173
174
175
176
        # 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
177
        self._set_zone_retry_timer(zone_name_class)
178

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

187
188
189
190
191
192
193
    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."""
194
        log_msg("Loading zone (%s, %s)" % zone_name_class)
195
196
197
198
199
200
        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
201
        zone_info["last_refresh_time"] = self._get_current_time()
202
        self._zonemgr_refresh_info[zone_name_class] = zone_info
203
204
205
206
        # 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)
207

Jerry's avatar
Jerry committed
208
    def _zone_is_expired(self, zone_name_class):
209
210
        """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
211
212
        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
213
214
215
216
217
            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
            return True

        return False

Jerry's avatar
Jerry committed
218
219
    def _get_zone_soa_rdata(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]
220

Jerry's avatar
Jerry committed
221
222
    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
223

Jerry's avatar
Jerry committed
224
225
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
226

Jerry's avatar
Jerry committed
227
228
    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
229
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
230
231
232

        return None

Jerry's avatar
Jerry committed
233
234
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
235

Jerry's avatar
Jerry committed
236
237
238
    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"]
239

Jerry's avatar
Jerry committed
240
241
    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
242

Jerry's avatar
Jerry committed
243
    def _set_zone_state(self, zone_name_class, zone_state):
244
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
245

Jerry's avatar
Jerry committed
246
247
    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
248

Jerry's avatar
Jerry committed
249
250
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
251

Jerry's avatar
Jerry committed
252
253
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
254

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

    def _send_command(self, module_name, command_name, params):
259
        """Send command between modules."""
260
        msg = create_command(command_name, params)
261
        try:
262
263
264
265
266
            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
267
        except socket.error:
268
            sys.stderr.write("[b10-zonemgr] Failed to send to module %s, the session has been closed." % module_name)
269

Jerry's avatar
Jerry committed
270
271
272
273
274
275
276
    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)
277
            # If hasn't received refresh response but are within refresh timeout, skip the zone
278
            if (ZONE_REFRESHING == zone_state and
Jerry's avatar
Jerry committed
279
                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
280
                continue
281
282
283
284

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

288
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
289
290
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
291

292
293
        return zone_need_refresh

294

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

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
318
        if not len(self._zonemgr_refresh_info):
319
320
321
322
            return True

        return False

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

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

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

366
367
368
369
370
371
372
373
374
375
376
377
378
    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
379
        start_event = threading.Event()
380
381

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

        # Return the thread to anyone interested
        return self._thread

Jerry's avatar
Jerry committed
392
    def shutdown(self):
393
394
395
396
397
398
399
400
401
        """
        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
402
        self._write_sock.send(b'shutdown') # make self._read_sock readble
403
404
405
406
407
408
        # 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
409

410
411
    def update_config_data(self, new_config):
        """ update ZonemgrRefresh config """
412
413
414
415
        # 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
416
        backup = self._zonemgr_refresh_info.copy()
417

418
        # Get a new value, but only if it is defined (commonly used below)
419
420
421
422
423
424
425
426
        # 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

427
428
        # store the values so we can restore them if there is a problem
        lowerbound_refresh_backup = self._lowerbound_refresh
429
430
        self._lowerbound_refresh = val_or_default(
            new_config.get('lowerbound_refresh'), self._lowerbound_refresh)
431
432

        lowerbound_retry_backup = self._lowerbound_retry
433
434
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
435
436

        max_transfer_timeout_backup = self._max_transfer_timeout
437
438
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
439
440

        refresh_jitter_backup = self._refresh_jitter
441
442
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
443
444

        reload_jitter_backup = self._reload_jitter
445
446
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
447
448
        try:
            required = {}
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
            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]
470
471
472
        # If we are not able to find it in database, restore the original
        except:
            self._zonemgr_refresh_info = backup
473
474
475
476
477
            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
478
            raise
479

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

491
        self._lock = threading.Lock()
492
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
493
        self.running = False
494
495

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

    def get_db_file(self):
Jerry's avatar
Jerry committed
509
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
510
511
512
        # 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)
513
514
515
516
517
        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):
518
519
        """Shutdown the zonemgr process. the thread which is keeping track of zone
        timers should be terminated.
520
        """
Jerry's avatar
Jerry committed
521
522
        self._zone_refresh.shutdown()

Jerry's avatar
Jerry committed
523
        self._slave_socket.close()
Jerry's avatar
Jerry committed
524
        self._master_socket.close()
525
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
526
        self.running = False
527
528

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

540
        self._config_data_check(complete)
541
        if self._zone_refresh is not None:
542
543
544
545
546
547
548
            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
549

550
551
        return answer

Jerry's avatar
Jerry committed
552
    def _config_data_check(self, config_data):
553
554
555
        """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
556
        # jitter should not be bigger than half of the original value
557
558
559
        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 "
560
                      "be set to 0.5")
Jerry's avatar
Jerry committed
561
562


Jerry's avatar
Jerry committed
563
    def _parse_cmd_params(self, args, command):
564
565
        zone_name = args.get("zone_name")
        if not zone_name:
566
            raise ZonemgrException("zone name should be provided")
567

Jerry's avatar
Jerry committed
568
569
        zone_class = args.get("zone_class")
        if not zone_class:
570
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
571
572
573
574

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

575
576
577
        master_str = args.get("master")
        if not master_str:
            raise ZonemgrException("master address should be provided")
578

Jerry's avatar
Jerry committed
579
        return ((zone_name, zone_class), master_str)
580
581
582


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

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

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

        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
621
        self.running = True
622
        while not self._shutdown_event.is_set():
623
            self._module_cc.check_command(False)
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650

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
651
        sys.stderr.write("[b10-zonemgr] exit zonemgr process\n")
652
    except isc.cc.session.SessionError as e:
653
        sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
654
                           "is the command channel daemon running?\n")
655
    except isc.cc.session.SessionTimeout as e:
656
        sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
657
                           "is the configuration manager running?\n")
658
    except isc.config.ModuleCCSessionError as e:
Jeremy C. Reed's avatar
Jeremy C. Reed committed
659
        sys.stderr.write("[b10-zonemgr] exit zonemgr process: %s\n" % str(e))
660

Michal Vaner's avatar
Michal Vaner committed
661
    if zonemgrd and zonemgrd.running:
662
663
        zonemgrd.shutdown()