zonemgr.py.in 29.6 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 *
42
from isc.notify import notify_out
Michal Vaner's avatar
Michal Vaner committed
43

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

48
49
50
51
52
# Pending system-wide debug level definitions, the ones we
# use here are hardcoded for now
DBG_PROCESS = logger.DBGLVL_TRACE_BASIC
DBG_COMMANDS = logger.DBGLVL_TRACE_DETAIL

53
54
55
56
# Constants for debug levels.
DBG_START_SHUT = logger.DBGLVL_START_SHUT
DBG_ZONEMGR_COMMAND = logger.DBGLVL_COMMAND
DBG_ZONEMGR_BASIC = logger.DBGLVL_TRACE_BASIC
57

58
isc.util.process.rename()
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

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

77
# define module name
Jerry's avatar
Jerry committed
78
79
XFRIN_MODULE_NAME = 'Xfrin'
AUTH_MODULE_NAME = 'Auth'
80

81
# define command name
82
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
Jerry's avatar
Jerry committed
83
ZONE_NOTIFY_COMMAND = 'notify'
84

85
86
87
88
89
# define zone state
ZONE_OK = 0
ZONE_REFRESHING = 1
ZONE_EXPIRED = 2

90
91
92
93
94
# offsets of fields in the SOA RDATA
REFRESH_OFFSET = 3
RETRY_OFFSET = 4
EXPIRED_OFFSET = 5

95
96
97
class ZonemgrException(Exception):
    pass

98
99
class ZonemgrRefresh:
    """This class will maintain and manage zone refresh info.
100
101
102
    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
103
    can be stopped by calling shutdown() in another thread.
104
    """
105

106
    def __init__(self, cc, db_file, slave_socket, module_cc_session):
107
        self._cc = cc
108
        self._check_sock = slave_socket
109
        self._db_file = db_file
110
        self._zonemgr_refresh_info = {}
111
112
113
114
115
        self._lowerbound_refresh = None
        self._lowerbound_retry = None
        self._max_transfer_timeout = None
        self._refresh_jitter = None
        self._reload_jitter = None
116
117
        self.update_config_data(module_cc_session.get_full_config(),
                                module_cc_session)
118
        self._running = False
119

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

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

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

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

Jerry's avatar
Jerry committed
147
    def _set_zone_retry_timer(self, zone_name_class):
148
        """Set zone next refresh time after zone refresh fail.
149
           now + retry - retry_jitter <= next_refresh_time <= now + retry
150
           """
151
152
153
154
        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
155
        zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
156
        self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
157

Jerry's avatar
Jerry committed
158
    def _set_zone_notify_timer(self, zone_name_class):
159
        """Set zone next refresh time after receiving notify
160
           next_refresh_time = now
161
        """
Jerry's avatar
Jerry committed
162
        self._set_zone_timer(zone_name_class, 0, 0)
163

Jerry's avatar
Jerry committed
164
    def _zone_not_exist(self, zone_name_class):
165
        """ Zone doesn't belong to zonemgr"""
166
        return not zone_name_class in self._zonemgr_refresh_info
Jerry's avatar
Jerry committed
167
168

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

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

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

203
204
205
206
207
208
209
    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."""
210
211

        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
212
213
        zone_info = {}
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
214
215
216
        if zone_soa is None:
            logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
            zone_info["zone_soa_rdata"] = None
217
            zone_reload_time = 0.0
218
219
        else:
            zone_info["zone_soa_rdata"] = zone_soa[7]
220
            zone_reload_time = float(zone_soa[7].split(" ")[RETRY_OFFSET])
221
        zone_info["zone_state"] = ZONE_OK
222
        zone_info["last_refresh_time"] = self._get_current_time()
223
        self._zonemgr_refresh_info[zone_name_class] = zone_info
224
        # Imposes some random jitters to avoid many zones need to do refresh at the same time.
225
226
        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)
227

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

        return False

Jerry's avatar
Jerry committed
238
239
    def _get_zone_soa_rdata(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]
240

Jerry's avatar
Jerry committed
241
242
    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
243

Jerry's avatar
Jerry committed
244
245
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
246

Jerry's avatar
Jerry committed
247
248
    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
249
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
250
251
252

        return None

Jerry's avatar
Jerry committed
253
254
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
255

Jerry's avatar
Jerry committed
256
257
258
    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"]
259

Jerry's avatar
Jerry committed
260
261
    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
262

Jerry's avatar
Jerry committed
263
    def _set_zone_state(self, zone_name_class, zone_state):
264
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
265

Jerry's avatar
Jerry committed
266
267
    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
268

Jerry's avatar
Jerry committed
269
270
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
271

Jerry's avatar
Jerry committed
272
273
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
274

Jerry's avatar
Jerry committed
275
276
    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
277
278

    def _send_command(self, module_name, command_name, params):
279
        """Send command between modules."""
280
        msg = create_command(command_name, params)
281
        try:
282
283
284
285
286
            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
287
        except socket.error:
288
            logger.error(ZONEMGR_SEND_FAIL, module_name)
289

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

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

309
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
310
311
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
312

313
314
        return zone_need_refresh

315

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

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
339
        if not len(self._zonemgr_refresh_info):
340
341
342
343
            return True

        return False

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

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

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

387
388
    def run_timer(self, daemon=False):
        """
389
390
        Keep track of zone timers. Spawns and starts a thread. The thread object
        is returned.
391
392
393
394
395

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
396
            logger.error(ZONEMGR_TIMER_THREAD_RUNNING)
397
398
399
400
401
            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
402
        start_event = threading.Event()
403
404

        # Start the thread
Michal Vaner's avatar
Michal Vaner committed
405
406
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
407
408
409
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
Michal Vaner's avatar
Michal Vaner committed
410
        start_event.wait()
411
412
413
414

        # Return the thread to anyone interested
        return self._thread

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

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

436
    def update_config_data(self, new_config, module_cc_session):
437
        """ update ZonemgrRefresh config """
438
        # Get a new value, but only if it is defined (commonly used below)
439
440
441
442
443
444
445
446
447
448
        # 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)
449

450
451
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
452

453
454
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
455

456
457
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
458

459
460
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
461

462
463
464
465
466
467
        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'):
468
469
470
                    if 'name' not in secondary_zone:
                        raise ZonemgrException("Secondary zone specified "
                                               "without a name")
471
                    name = secondary_zone['name']
472
473
474
475
476
477
478
479
480
481

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

482
483
484
485
486
487
488
489
490
491
492
                    # 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:
493
                        rr_class = secondary_zone['class']
494
                    else:
495
496
497
498
499
                        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())
500
                    except isc.dns.InvalidRRClass:
501
502
503
                        raise ZonemgrException("Bad RR class '" +
                                               rr_class +
                                               "' for zone " + name)
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
                    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
519

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

531
        self._lock = threading.Lock()
532
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
533
        self.running = False
534
535

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

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

Jerry's avatar
Jerry committed
563
        self._slave_socket.close()
Jerry's avatar
Jerry committed
564
        self._master_socket.close()
565
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
566
        self.running = False
567
568

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

580
        self._config_data_check(complete)
581
        if self._zone_refresh is not None:
582
            try:
583
                self._zone_refresh.update_config_data(complete, self._module_cc)
584
585
586
587
588
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete
589

590
591
        return answer

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

Jerry's avatar
Jerry committed
601
    def _parse_cmd_params(self, args, command):
602
603
        zone_name = args.get("zone_name")
        if not zone_name:
604
            logger.error(ZONEMGR_NO_ZONE_NAME)
605
            raise ZonemgrException("zone name should be provided")
606

Jerry's avatar
Jerry committed
607
608
        zone_class = args.get("zone_class")
        if not zone_class:
609
            logger.error(ZONEMGR_NO_ZONE_CLASS)
610
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
611
612
613
614

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

615
616
        master_str = args.get("master")
        if not master_str:
617
            logger.error(ZONEMGR_NO_MASTER_ADDRESS)
618
            raise ZonemgrException("master address should be provided")
619

Jerry's avatar
Jerry committed
620
        return ((zone_name, zone_class), master_str)
621
622
623


    def command_handler(self, command, args):
624
        """Handle command receivd from command channel.
625
        ZONE_NOTIFY_COMMAND is issued by Auth process;
626
        ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
627
628
        Xfrin process;
        shutdown is issued by a user or Boss process. """
629
        answer = create_answer(0)
Jerry's avatar
Jerry committed
630
        if command == ZONE_NOTIFY_COMMAND:
Jerry's avatar
Jerry committed
631
            """ Handle Auth notify command"""
632
            # master is the source sender of the notify message.
Jerry's avatar
Jerry committed
633
            zone_name_class, master = self._parse_cmd_params(args, command)
634
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY, zone_name_class[0], zone_name_class[1])
635
            with self._lock:
636
                self._zone_refresh.zone_handle_notify(zone_name_class, master)
637
            # Send notification to zonemgr timer thread
Jerry's avatar
Jerry committed
638
            self._master_socket.send(b" ")# make self._slave_socket readble
639

640
        elif command == notify_out.ZONE_NEW_DATA_READY_CMD:
641
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
642
            zone_name_class = self._parse_cmd_params(args, command)
643
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
644
            with self._lock:
645
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jerry's avatar
Jerry committed
646
            self._master_socket.send(b" ")# make self._slave_socket readble
647

648
        elif command == notify_out.ZONE_XFRIN_FAILED:
Jerry's avatar
Jerry committed
649
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
650
            zone_name_class = self._parse_cmd_params(args, command)
651
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])
652
            with self._lock:
653
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jerry's avatar
Jerry committed
654
            self._master_socket.send(b" ")# make self._slave_socket readble
655
656

        elif command == "shutdown":
657
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
658
659
660
            self.shutdown()

        else:
661
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
662
663
664
665
666
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
667
        logger.debug(DBG_PROCESS, ZONEMGR_STARTED)
Michal Vaner's avatar
Michal Vaner committed
668
        self.running = True
Jelte Jansen's avatar
Jelte Jansen committed
669
670
671
672
673
        try:
            while not self._shutdown_event.is_set():
                self._module_cc.check_command(False)
        finally:
            self._module_cc.send_stopping()
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691

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:
692
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
693
694
695
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
696
        if options.verbose:
697
            logger.set_severity("DEBUG", 99)
698
699
700
701
702

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
703
704
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

705
    except isc.cc.session.SessionError as e:
706
707
        logger.error(ZONEMGR_SESSION_ERROR)

708
    except isc.cc.session.SessionTimeout as e:
709
710
        logger.error(ZONEMGR_SESSION_TIMEOUT)

711
    except isc.config.ModuleCCSessionError as e:
712
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))
713

Michal Vaner's avatar
Michal Vaner committed
714
    if zonemgrd and zonemgrd.running:
715
716
        zonemgrd.shutdown()

717
    logger.debug(DBG_START_SHUT, ZONEMGR_SHUTDOWN)