zonemgr.py.in 29.4 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
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
31
import isc.dns
32
33
34
35
import random
import threading
import select
import socket
36
import errno
37
38
39
from isc.datasrc import sqlite3_ds
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
40
import isc.util.process
41
from isc.log_messages.zonemgr_messages import *
Michal Vaner's avatar
Michal Vaner committed
42

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

47
48
49
50
# Constants for debug levels.
DBG_START_SHUT = logger.DBGLVL_START_SHUT
DBG_ZONEMGR_COMMAND = logger.DBGLVL_COMMAND
DBG_ZONEMGR_BASIC = logger.DBGLVL_TRACE_BASIC
51

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

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

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

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

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

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

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
    can be stopped by calling shutdown() in another thread.
100
    """
101

102
    def __init__(self, cc, db_file, slave_socket, module_cc_session):
103
        self._cc = cc
104
        self._check_sock = slave_socket
105
        self._db_file = db_file
106
        self._zonemgr_refresh_info = {}
107
108
109
110
111
        self._lowerbound_refresh = None
        self._lowerbound_retry = None
        self._max_transfer_timeout = None
        self._refresh_jitter = None
        self._reload_jitter = None
112
113
        self.update_config_data(module_cc_session.get_full_config(),
                                module_cc_session)
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
148
149
150
        if (self._get_zone_soa_rdata(zone_name_class) is not None):
            zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[RETRY_OFFSET])
        else:
            zone_retry_time = 0.0
151
        zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
152
        self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
153

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

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

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

Jerry's avatar
Jerry committed
175
    def zone_refresh_fail(self, zone_name_class):
176
        """Update zone info after zone refresh fail"""
Jerry's avatar
Jerry committed
177
        if (self._zone_not_exist(zone_name_class)):
178
            logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0], zone_name_class[1])
179
180
            raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
181
        # Is zone expired?
182
        if ((self._get_zone_soa_rdata(zone_name_class) is None) or
183
            self._zone_is_expired(zone_name_class)):
184
185
186
            self._set_zone_state(zone_name_class, ZONE_EXPIRED)
        else:
            self._set_zone_state(zone_name_class, ZONE_OK)
Jerry's avatar
Jerry committed
187
        self._set_zone_retry_timer(zone_name_class)
188

Jerry's avatar
Jerry committed
189
    def zone_handle_notify(self, zone_name_class, master):
190
        """Handle zone notify"""
Jerry's avatar
Jerry committed
191
        if (self._zone_not_exist(zone_name_class)):
192
            logger.error(ZONEMGR_UNKNOWN_ZONE_NOTIFIED, zone_name_class[0], zone_name_class[1])
193
194
            raise ZonemgrException("[b10-zonemgr] Notified zone (%s, %s) "
                                   "doesn't belong to zonemgr" % zone_name_class)
Jerry's avatar
Jerry committed
195
196
        self._set_zone_notifier_master(zone_name_class, master)
        self._set_zone_notify_timer(zone_name_class)
197

198
199
200
201
202
203
204
    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."""
205
206

        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
207
208
        zone_info = {}
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
209
210
211
        if zone_soa is None:
            logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
            zone_info["zone_soa_rdata"] = None
212
            zone_reload_time = 0.0
213
214
        else:
            zone_info["zone_soa_rdata"] = zone_soa[7]
215
            zone_reload_time = float(zone_soa[7].split(" ")[RETRY_OFFSET])
216
        zone_info["zone_state"] = ZONE_OK
217
        zone_info["last_refresh_time"] = self._get_current_time()
218
        self._zonemgr_refresh_info[zone_name_class] = zone_info
219
        # Imposes some random jitters to avoid many zones need to do refresh at the same time.
220
221
        zone_reload_time = max(self._lowerbound_retry, zone_reload_time)
        self._set_zone_timer(zone_name_class, zone_reload_time, self._reload_jitter * zone_reload_time)
222

Jerry's avatar
Jerry committed
223
    def _zone_is_expired(self, zone_name_class):
224
225
        """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
226
227
        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
228
229
230
231
232
            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
            return True

        return False

Jerry's avatar
Jerry committed
233
234
    def _get_zone_soa_rdata(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]
235

Jerry's avatar
Jerry committed
236
237
    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
238

Jerry's avatar
Jerry committed
239
240
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
241

Jerry's avatar
Jerry committed
242
243
    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
244
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
245
246
247

        return None

Jerry's avatar
Jerry committed
248
249
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
250

Jerry's avatar
Jerry committed
251
252
253
    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"]
254

Jerry's avatar
Jerry committed
255
256
    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
257

Jerry's avatar
Jerry committed
258
    def _set_zone_state(self, zone_name_class, zone_state):
259
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
260

Jerry's avatar
Jerry committed
261
262
    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
263

Jerry's avatar
Jerry committed
264
265
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
266

Jerry's avatar
Jerry committed
267
268
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
269

Jerry's avatar
Jerry committed
270
271
    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
272
273

    def _send_command(self, module_name, command_name, params):
274
        """Send command between modules."""
275
        msg = create_command(command_name, params)
276
        try:
277
278
279
280
281
            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
282
        except socket.error:
283
            logger.error(ZONEMGR_SEND_FAIL, module_name)
284

Jerry's avatar
Jerry committed
285
286
287
288
289
290
291
    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)
292
293
            # If hasn't received refresh response but are within refresh
            # timeout, skip the zone
294
            if (ZONE_REFRESHING == zone_state and
Jerry's avatar
Jerry committed
295
                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
296
                continue
297
298
299
300

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

304
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
305
306
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
307

308
309
        return zone_need_refresh

310

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

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
334
        if not len(self._zonemgr_refresh_info):
335
336
337
338
            return True

        return False

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

362
            """ Wait for the socket notification for a maximum time of timeout
363
            in seconds (as float)."""
364
            try:
Jerry's avatar
Jerry committed
365
                rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
366
367
368
369
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                else:
370
                    logger.error(ZONEMGR_SELECT_ERROR, e);
371
372
                    break

Jerry's avatar
Jerry committed
373
            for fd in rlist:
374
                if fd == self._read_sock: # awaken by shutdown socket
375
                    # self._running will be False by now, if it is not a false
Michal Vaner's avatar
Michal Vaner committed
376
377
                    # alarm (linux kernel is said to trigger spurious wakeup
                    # on a filehandle that is not really readable).
378
                    continue
Jerry's avatar
Jerry committed
379
380
                if fd == self._check_sock: # awaken by check socket
                    self._check_sock.recv(32)
Jerry's avatar
Jerry committed
381

382
383
    def run_timer(self, daemon=False):
        """
384
385
        Keep track of zone timers. Spawns and starts a thread. The thread object
        is returned.
386
387
388
389
390

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
391
            logger.error(ZONEMGR_TIMER_THREAD_RUNNING)
392
393
394
395
396
            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
397
        start_event = threading.Event()
398
399

        # Start the thread
Michal Vaner's avatar
Michal Vaner committed
400
401
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
402
403
404
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
Michal Vaner's avatar
Michal Vaner committed
405
        start_event.wait()
406
407
408
409

        # Return the thread to anyone interested
        return self._thread

Jerry's avatar
Jerry committed
410
    def shutdown(self):
411
412
413
414
415
        """
        Stop the run_timer() thread. Block until it finished. This must be
        called from a different thread.
        """
        if not self._running:
416
            logger.error(ZONEMGR_NO_TIMER_THREAD)
417
418
419
420
            raise RuntimeError("Trying to shutdown, but not running")

        # Ask the thread to stop
        self._running = False
Jerry's avatar
Jerry committed
421
        self._write_sock.send(b'shutdown') # make self._read_sock readble
422
423
424
425
        # Wait for it to actually finnish
        self._thread.join()
        # Wipe out what we do not need
        self._thread = None
426
427
        self._read_sock.close()
        self._write_sock.close()
428
429
        self._read_sock = None
        self._write_sock = None
430

431
    def update_config_data(self, new_config, module_cc_session):
432
        """ update ZonemgrRefresh config """
433
        # Get a new value, but only if it is defined (commonly used below)
434
435
436
437
438
439
440
441
442
443
        # 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

        self._lowerbound_refresh = val_or_default(
            new_config.get('lowerbound_refresh'), self._lowerbound_refresh)
444

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

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

451
452
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
453

454
455
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
456

457
458
459
460
461
462
        try:
            required = {}
            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'):
463
464
465
                    if 'name' not in secondary_zone:
                        raise ZonemgrException("Secondary zone specified "
                                               "without a name")
466
                    name = secondary_zone['name']
467
468
469
470
471
472
473
474
475
476

                    # Convert to Name and back (both to check and to normalize)
                    try:
                        name = isc.dns.Name(name, True).to_text()
                    # Name() can raise a number of different exceptions, just
                    # catch 'em all.
                    except Exception as isce:
                        raise ZonemgrException("Bad zone name '" + name +
                                               "': " + str(isce))

477
478
479
480
481
482
483
484
485
486
487
                    # Currently we use an explicit get_default_value call
                    # in case the class hasn't been set. Alternatively, we
                    # could use
                    # module_cc_session.get_value('secondary_zones[INDEX]/class')
                    # To get either the value that was set, or the default if
                    # it wasn't set.
                    # But the real solution would be to make new_config a type
                    # that contains default values itself
                    # (then this entire method can be simplified a lot, and we
                    # wouldn't need direct access to the ccsession object)
                    if 'class' in secondary_zone:
488
                        rr_class = secondary_zone['class']
489
                    else:
490
491
492
493
494
                        rr_class = module_cc_session.get_default_value(
                                        'secondary_zones/class')
                    # Convert rr_class to and from RRClass to check its value
                    try:
                        name_class = (name, isc.dns.RRClass(rr_class).to_text())
495
                    except isc.dns.InvalidRRClass:
496
497
498
                        raise ZonemgrException("Bad RR class '" +
                                               rr_class +
                                               "' for zone " + name)
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
                    required[name_class] = True
                    # Add it only if it isn't there already
                    if not name_class in self._zonemgr_refresh_info:
                        # If we are not able to find it in database, log an warning
                        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]
        except:
            raise
514

515
516
517
class Zonemgr:
    """Zone manager class."""
    def __init__(self):
518
        self._zone_refresh = None
519
520
        self._setup_session()
        self._db_file = self.get_db_file()
521
        # Create socket pair for communicating between main thread and zonemgr timer thread
Jerry's avatar
Jerry committed
522
        self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
523
        self._zone_refresh = ZonemgrRefresh(self._cc, self._db_file, self._slave_socket, self._module_cc)
524
        self._zone_refresh.run_timer()
525

526
        self._lock = threading.Lock()
527
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
528
        self.running = False
529
530

    def _setup_session(self):
531
        """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
532
533
        commands and config data sent from other modules, another one (self._cc)
        is used to send commands to proper modules."""
534
535
536
537
        self._cc = isc.cc.Session()
        self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                                  self.config_handler,
                                                  self.command_handler)
538
        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
539
        self._config_data = self._module_cc.get_full_config()
Jerry's avatar
Jerry committed
540
        self._config_data_check(self._config_data)
541
542
543
        self._module_cc.start()

    def get_db_file(self):
Jerry's avatar
Jerry committed
544
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
545
546
547
        # 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)
548
549
550
551
552
        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):
553
554
        """Shutdown the zonemgr process. The thread which is keeping track of
           zone timers should be terminated.
555
        """
Jerry's avatar
Jerry committed
556
557
        self._zone_refresh.shutdown()

Jerry's avatar
Jerry committed
558
        self._slave_socket.close()
Jerry's avatar
Jerry committed
559
        self._master_socket.close()
560
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
561
        self.running = False
562
563

    def config_handler(self, new_config):
564
        """ Update config data. """
565
        answer = create_answer(0)
566
        ok = True
567
        complete = self._config_data.copy()
568
        for key in new_config:
569
            if key not in complete:
570
                answer = create_answer(1, "Unknown config data: " + str(key))
571
                ok = False
572
                continue
573
            complete[key] = new_config[key]
574

575
        self._config_data_check(complete)
576
        if self._zone_refresh is not None:
577
            try:
578
                self._zone_refresh.update_config_data(complete, self._module_cc)
579
580
581
582
583
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete
584

585
586
        return answer

Jerry's avatar
Jerry committed
587
    def _config_data_check(self, config_data):
588
589
590
        """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
591
        # jitter should not be bigger than half of the original value
592
593
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
594
            logger.warn(ZONEMGR_JITTER_TOO_BIG)
Jerry's avatar
Jerry committed
595

Jerry's avatar
Jerry committed
596
    def _parse_cmd_params(self, args, command):
597
598
        zone_name = args.get("zone_name")
        if not zone_name:
599
            logger.error(ZONEMGR_NO_ZONE_NAME)
600
            raise ZonemgrException("zone name should be provided")
601

Jerry's avatar
Jerry committed
602
603
        zone_class = args.get("zone_class")
        if not zone_class:
604
            logger.error(ZONEMGR_NO_ZONE_CLASS)
605
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
606
607
608
609

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

610
611
        master_str = args.get("master")
        if not master_str:
612
            logger.error(ZONEMGR_NO_MASTER_ADDRESS)
613
            raise ZonemgrException("master address should be provided")
614

Jerry's avatar
Jerry committed
615
        return ((zone_name, zone_class), master_str)
616
617
618


    def command_handler(self, command, args):
619
        """Handle command receivd from command channel.
620
621
622
623
        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. """
624
        answer = create_answer(0)
Jerry's avatar
Jerry committed
625
        if command == ZONE_NOTIFY_COMMAND:
Jerry's avatar
Jerry committed
626
            """ Handle Auth notify command"""
627
            # master is the source sender of the notify message.
Jerry's avatar
Jerry committed
628
            zone_name_class, master = self._parse_cmd_params(args, command)
629
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY, zone_name_class[0], zone_name_class[1])
630
            with self._lock:
631
                self._zone_refresh.zone_handle_notify(zone_name_class, master)
632
            # Send notification to zonemgr timer thread
Jerry's avatar
Jerry committed
633
            self._master_socket.send(b" ")# make self._slave_socket readble
634

Jerry's avatar
Jerry committed
635
        elif command == ZONE_XFRIN_SUCCESS_COMMAND:
636
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
637
            zone_name_class = self._parse_cmd_params(args, command)
638
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
639
            with self._lock:
640
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jerry's avatar
Jerry committed
641
            self._master_socket.send(b" ")# make self._slave_socket readble
642

Jerry's avatar
Jerry committed
643
        elif command == ZONE_XFRIN_FAILED_COMMAND:
Jerry's avatar
Jerry committed
644
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
645
            zone_name_class = self._parse_cmd_params(args, command)
646
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])
647
            with self._lock:
648
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jerry's avatar
Jerry committed
649
            self._master_socket.send(b" ")# make self._slave_socket readble
650
651

        elif command == "shutdown":
652
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
653
654
655
            self.shutdown()

        else:
656
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
657
658
659
660
661
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
Michal Vaner's avatar
Michal Vaner committed
662
        self.running = True
Jelte Jansen's avatar
Jelte Jansen committed
663
664
665
666
667
        try:
            while not self._shutdown_event.is_set():
                self._module_cc.check_command(False)
        finally:
            self._module_cc.send_stopping()
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685

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:
686
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
687
688
689
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
690
        if options.verbose:
691
            logger.set_severity("DEBUG", 99)
692
693
694
695
696

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
697
698
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

699
    except isc.cc.session.SessionError as e:
700
701
        logger.error(ZONEMGR_SESSION_ERROR)

702
    except isc.cc.session.SessionTimeout as e:
703
704
        logger.error(ZONEMGR_SESSION_TIMEOUT)

705
    except isc.config.ModuleCCSessionError as e:
706
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))
707

Michal Vaner's avatar
Michal Vaner committed
708
    if zonemgrd and zonemgrd.running:
709
710
        zonemgrd.shutdown()

711
    logger.debug(DBG_START_SHUT, ZONEMGR_SHUTDOWN)