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

# Copyright (C) 2010  Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

18
"""
19
20
21
22
23
24
25
26
27
28
29
30
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
# Initialize logging for called modules.
45
isc.log.init("b10-zonemgr", buffer=True)
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, db_file, slave_socket, module_cc_session):
107
        self._mccs = module_cc_session
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
        """Handle an incomding NOTIFY message via the Auth module.

        It returns True if the specified zone matches one of the locally
        configured list of secondary zones; otherwise returns False.
        In the latter case it assumes the server is a primary (master) of the
        zone; the Auth module should have rejected the case where it's not
        even authoritative for the zone.

        Note: to be more robust and less independent from other module's
        behavior, it's probably safer to check the authority condition here,
        too.  But right now it uses SQLite3 specific API (to be deprecated),
        so we rather rely on Auth.

        Parameters:
        zone_name_class (Name, RRClass): the notified zone name and class.
        master (str): textual address of the NOTIFY sender.

        """
        if self._zone_not_exist(zone_name_class):
213
214
            logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_ZONE_NOTIFY_NOT_SECONDARY,
                         zone_name_class[0], zone_name_class[1], master)
215
            return False
Jerry's avatar
Jerry committed
216
217
        self._set_zone_notifier_master(zone_name_class, master)
        self._set_zone_notify_timer(zone_name_class)
218
        return True
219

220
221
222
223
224
225
226
    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."""
227
228

        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
229
230
        zone_info = {}
        zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
231
232
233
        if zone_soa is None:
            logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
            zone_info["zone_soa_rdata"] = None
234
            zone_reload_time = 0.0
235
236
        else:
            zone_info["zone_soa_rdata"] = zone_soa[7]
237
            zone_reload_time = float(zone_soa[7].split(" ")[RETRY_OFFSET])
238
        zone_info["zone_state"] = ZONE_OK
239
        zone_info["last_refresh_time"] = self._get_current_time()
240
        self._zonemgr_refresh_info[zone_name_class] = zone_info
241
        # Imposes some random jitters to avoid many zones need to do refresh at the same time.
242
243
        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)
244

Jerry's avatar
Jerry committed
245
    def _zone_is_expired(self, zone_name_class):
246
247
        """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
248
249
        zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
250
251
252
253
254
            zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
            return True

        return False

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

Jerry's avatar
Jerry committed
258
259
    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
260

Jerry's avatar
Jerry committed
261
262
    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
263

Jerry's avatar
Jerry committed
264
265
    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
266
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
267
268
269

        return None

Jerry's avatar
Jerry committed
270
271
    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
272

Jerry's avatar
Jerry committed
273
274
275
    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"]
276

Jerry's avatar
Jerry committed
277
278
    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
279

Jerry's avatar
Jerry committed
280
    def _set_zone_state(self, zone_name_class, zone_state):
281
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
282

Jerry's avatar
Jerry committed
283
284
    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
285

Jerry's avatar
Jerry committed
286
287
    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
288

Jerry's avatar
Jerry committed
289
290
    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
291

Jerry's avatar
Jerry committed
292
293
    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
294
295

    def _send_command(self, module_name, command_name, params):
296
        """Send command between modules."""
297
        try:
298
            self._mccs.rpc_call(command_name, module_name, params=params)
299
300
301
302
        except socket.error:
            # FIXME: WTF? Where does socket.error come from? And how do we ever
            # dare ignore such serious error? It can only be broken link to
            # msgq, we need to terminate then.
303
            logger.error(ZONEMGR_SEND_FAIL, module_name)
304
305
        except (isc.cc.session.SessionTimeout, isc.config.RPCError):
            pass        # for now we just ignore the failure
306

Jerry's avatar
Jerry committed
307
308
309
310
311
312
313
    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)
314
315
            # If hasn't received refresh response but are within refresh
            # timeout, skip the zone
316
            if (ZONE_REFRESHING == zone_state and
Jerry's avatar
Jerry committed
317
                (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
318
                continue
319
320
321
322

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

326
            # Find the zone need do refresh
Jerry's avatar
Jerry committed
327
328
            if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
                break
329

330
331
        return zone_need_refresh

332

Jerry's avatar
Jerry committed
333
    def _do_refresh(self, zone_name_class):
334
        """Do zone refresh."""
335
        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE, zone_name_class[0], zone_name_class[1])
Jerry's avatar
Jerry committed
336
        self._set_zone_state(zone_name_class, ZONE_REFRESHING)
337
        self._set_zone_refresh_timeout(zone_name_class, self._get_current_time() + self._max_transfer_timeout)
Jerry's avatar
Jerry committed
338
        notify_master = self._get_zone_notifier_master(zone_name_class)
339
        # If the zone has notify master, send notify command to xfrin module
340
        if notify_master:
Jerry's avatar
Jerry committed
341
342
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1],
343
                     "master" : notify_master
344
                     }
345
            self._send_command(XFRIN_MODULE_NAME, ZONE_NOTIFY_COMMAND, param)
Jerry's avatar
Jerry committed
346
            self._clear_zone_notifier_master(zone_name_class)
347
348
        # Send refresh command to xfrin module
        else:
Jerry's avatar
Jerry committed
349
350
351
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1]
                    }
Jerry's avatar
Jerry committed
352
            self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)
353
354
355

    def _zone_mgr_is_empty(self):
        """Does zone manager has no zone?"""
Jerry's avatar
Jerry committed
356
        if not len(self._zonemgr_refresh_info):
357
358
359
360
            return True

        return False

Michal Vaner's avatar
Michal Vaner committed
361
    def _run_timer(self, start_event):
362
        while self._running:
Michal Vaner's avatar
Michal Vaner committed
363
364
            # 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
365
            # a race condition and _running could be set to false before we
Michal Vaner's avatar
Michal Vaner committed
366
367
368
369
            # could enter it
            if start_event:
                start_event.set()
                start_event = None
Jerry's avatar
Jerry committed
370
            # If zonemgr has no zone, set timer timeout to self._lowerbound_retry.
371
            if self._zone_mgr_is_empty():
372
                timeout = self._lowerbound_retry
373
            else:
374
                zone_need_refresh = self._find_need_do_refresh_zone()
375
                # If don't get zone with minimum next refresh time, set timer timeout to self._lowerbound_retry.
376
                if not zone_need_refresh:
377
                    timeout = self._lowerbound_retry
378
379
380
381
382
                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
383

384
            """ Wait for the socket notification for a maximum time of timeout
385
            in seconds (as float)."""
386
            try:
Jerry's avatar
Jerry committed
387
                rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
388
389
390
391
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                else:
392
                    logger.error(ZONEMGR_SELECT_ERROR, e);
393
394
                    break

Jerry's avatar
Jerry committed
395
            for fd in rlist:
396
                if fd == self._read_sock: # awaken by shutdown socket
397
                    # self._running will be False by now, if it is not a false
Michal Vaner's avatar
Michal Vaner committed
398
399
                    # alarm (linux kernel is said to trigger spurious wakeup
                    # on a filehandle that is not really readable).
400
                    continue
Jerry's avatar
Jerry committed
401
402
                if fd == self._check_sock: # awaken by check socket
                    self._check_sock.recv(32)
Jerry's avatar
Jerry committed
403

404
405
    def run_timer(self, daemon=False):
        """
406
407
        Keep track of zone timers. Spawns and starts a thread. The thread object
        is returned.
408
409
410
411
412

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
413
            logger.error(ZONEMGR_TIMER_THREAD_RUNNING)
414
415
416
417
418
            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
419
        start_event = threading.Event()
420
421

        # Start the thread
Michal Vaner's avatar
Michal Vaner committed
422
423
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
424
425
426
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
Michal Vaner's avatar
Michal Vaner committed
427
        start_event.wait()
428
429
430
431

        # Return the thread to anyone interested
        return self._thread

Jerry's avatar
Jerry committed
432
    def shutdown(self):
433
434
435
436
437
        """
        Stop the run_timer() thread. Block until it finished. This must be
        called from a different thread.
        """
        if not self._running:
438
            logger.error(ZONEMGR_NO_TIMER_THREAD)
439
440
441
442
            raise RuntimeError("Trying to shutdown, but not running")

        # Ask the thread to stop
        self._running = False
Jelte Jansen's avatar
Jelte Jansen committed
443
        self._write_sock.send(b'shutdown') # make self._read_sock readable
444
445
446
447
        # Wait for it to actually finnish
        self._thread.join()
        # Wipe out what we do not need
        self._thread = None
448
449
        self._read_sock.close()
        self._write_sock.close()
450
451
        self._read_sock = None
        self._write_sock = None
452

453
    def update_config_data(self, new_config, module_cc_session):
454
        """ update ZonemgrRefresh config """
455
        # Get a new value, but only if it is defined (commonly used below)
456
457
458
459
460
461
462
463
464
465
        # 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)
466

467
468
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
469

470
471
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
472

473
474
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
475

476
477
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
478

479
480
481
482
483
484
        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'):
485
486
487
                    if 'name' not in secondary_zone:
                        raise ZonemgrException("Secondary zone specified "
                                               "without a name")
488
                    name = secondary_zone['name']
489
490
491
492
493
494
495
496
497
498

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

499
500
501
502
503
504
505
506
507
508
509
                    # 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:
510
                        rr_class = secondary_zone['class']
511
                    else:
512
513
514
515
516
                        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())
517
                    except isc.dns.InvalidRRClass:
518
519
520
                        raise ZonemgrException("Bad RR class '" +
                                               rr_class +
                                               "' for zone " + name)
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
                    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
536

537
538
539
class Zonemgr:
    """Zone manager class."""
    def __init__(self):
540
        self._zone_refresh = None
541
542
        self._setup_session()
        self._db_file = self.get_db_file()
543
        # Create socket pair for communicating between main thread and zonemgr timer thread
Jerry's avatar
Jerry committed
544
        self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
545
        self._zone_refresh = ZonemgrRefresh(self._db_file, self._slave_socket, self._module_cc)
546
        self._zone_refresh.run_timer()
547

548
        self._lock = threading.Lock()
549
        self._shutdown_event = threading.Event()
Michal Vaner's avatar
Michal Vaner committed
550
        self.running = False
551
552

    def _setup_session(self):
553
        """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
554
555
        commands and config data sent from other modules, another one (self._cc)
        is used to send commands to proper modules."""
556
557
558
        self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                                  self.config_handler,
                                                  self.command_handler)
559
        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
560
        self._config_data = self._module_cc.get_full_config()
Jerry's avatar
Jerry committed
561
        self._config_data_check(self._config_data)
562
563
564
        self._module_cc.start()

    def get_db_file(self):
Jerry's avatar
Jerry committed
565
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
566
567
568
        # 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)
569
570
571
572
573
        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):
574
575
        """Shutdown the zonemgr process. The thread which is keeping track of
           zone timers should be terminated.
576
        """
Jerry's avatar
Jerry committed
577
578
        self._zone_refresh.shutdown()

Jerry's avatar
Jerry committed
579
        self._slave_socket.close()
Jerry's avatar
Jerry committed
580
        self._master_socket.close()
581
        self._shutdown_event.set()
Michal Vaner's avatar
Michal Vaner committed
582
        self.running = False
583
584

    def config_handler(self, new_config):
585
        """ Update config data. """
586
        answer = create_answer(0)
587
        ok = True
588
        complete = self._config_data.copy()
589
        for key in new_config:
590
            if key not in complete:
591
                answer = create_answer(1, "Unknown config data: " + str(key))
592
                ok = False
593
                continue
594
            complete[key] = new_config[key]
595

596
        self._config_data_check(complete)
597
        if self._zone_refresh is not None:
598
            try:
599
                self._zone_refresh.update_config_data(complete, self._module_cc)
600
601
602
603
604
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete
605

606
607
        return answer

Jerry's avatar
Jerry committed
608
    def _config_data_check(self, config_data):
609
610
611
        """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
612
        # jitter should not be bigger than half of the original value
613
614
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
615
            logger.warn(ZONEMGR_JITTER_TOO_BIG)
Jerry's avatar
Jerry committed
616

Jerry's avatar
Jerry committed
617
    def _parse_cmd_params(self, args, command):
618
619
        zone_name = args.get("zone_name")
        if not zone_name:
620
            logger.error(ZONEMGR_NO_ZONE_NAME)
621
            raise ZonemgrException("zone name should be provided")
622

Jerry's avatar
Jerry committed
623
624
        zone_class = args.get("zone_class")
        if not zone_class:
625
            logger.error(ZONEMGR_NO_ZONE_CLASS)
626
            raise ZonemgrException("zone class should be provided")
Jerry's avatar
Jerry committed
627
628
629
630

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

631
632
        master_str = args.get("master")
        if not master_str:
633
            logger.error(ZONEMGR_NO_MASTER_ADDRESS)
634
            raise ZonemgrException("master address should be provided")
635

Jerry's avatar
Jerry committed
636
        return ((zone_name, zone_class), master_str)
637
638
639


    def command_handler(self, command, args):
640
        """Handle command receivd from command channel.
641
        ZONE_NOTIFY_COMMAND is issued by Auth process;
642
        ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
643
        Xfrin process;
Jelte Jansen's avatar
Jelte Jansen committed
644
        shutdown is issued by a user or Init process. """
645
        answer = create_answer(0)
Jerry's avatar
Jerry committed
646
        if command == ZONE_NOTIFY_COMMAND:
Jerry's avatar
Jerry committed
647
            """ Handle Auth notify command"""
648
            # master is the source sender of the notify message.
Jerry's avatar
Jerry committed
649
            zone_name_class, master = self._parse_cmd_params(args, command)
650
651
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY,
                         zone_name_class[0], zone_name_class[1])
652
            with self._lock:
653
654
655
656
                need_refresh = self._zone_refresh.zone_handle_notify(
                    zone_name_class, master)
            if need_refresh:
                # Send notification to zonemgr timer thread by making
Jelte Jansen's avatar
Jelte Jansen committed
657
                # self._slave_socket readable.
658
                self._master_socket.send(b" ")
659

660
        elif command == notify_out.ZONE_NEW_DATA_READY_CMD:
661
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
662
            zone_name_class = self._parse_cmd_params(args, command)
663
664
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS,
                         zone_name_class[0], zone_name_class[1])
665
            with self._lock:
666
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jelte Jansen's avatar
Jelte Jansen committed
667
            self._master_socket.send(b" ")# make self._slave_socket readable
668

669
        elif command == notify_out.ZONE_XFRIN_FAILED:
Jerry's avatar
Jerry committed
670
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
671
            zone_name_class = self._parse_cmd_params(args, command)
672
673
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED,
                         zone_name_class[0], zone_name_class[1])
674
            with self._lock:
675
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jelte Jansen's avatar
Jelte Jansen committed
676
            self._master_socket.send(b" ")# make self._slave_socket readable
677
678

        elif command == "shutdown":
679
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
680
681
682
            self.shutdown()

        else:
683
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
684
685
686
687
688
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
689
        logger.debug(DBG_PROCESS, ZONEMGR_STARTED)
Michal Vaner's avatar
Michal Vaner committed
690
        self.running = True
Jelte Jansen's avatar
Jelte Jansen committed
691
692
        try:
            while not self._shutdown_event.is_set():
693
694
695
696
697
698
699
700
701
702
703
704
705
                fileno = self._module_cc.get_socket().fileno()
                # Wait with select() until there is something to read,
                # and then read it using a non-blocking read
                # This may or may not be relevant data for this loop,
                # but due to the way the zonemgr does threading, we
                # can't have a blocking read loop here.
                try:
                    (reads, _, _) = select.select([fileno], [], [])
                except select.error as se:
                    if se.args[0] != errno.EINTR:
                        raise
                if fileno in reads:
                    self._module_cc.check_command(True)
Jelte Jansen's avatar
Jelte Jansen committed
706
707
        finally:
            self._module_cc.send_stopping()
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725

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:
726
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
727
728
729
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
730
        if options.verbose:
731
            logger.set_severity("DEBUG", 99)
732
733
734
735
736

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
737
738
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

739
    except isc.cc.session.SessionError as e:
740
741
        logger.error(ZONEMGR_SESSION_ERROR)

742
    except isc.cc.session.SessionTimeout as e:
743
744
        logger.error(ZONEMGR_SESSION_TIMEOUT)

745
    except isc.config.ModuleCCSessionError as e:
746
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))
747

Michal Vaner's avatar
Michal Vaner committed
748
    if zonemgrd and zonemgrd.running:
749
750
        zonemgrd.shutdown()

751
    logger.info(ZONEMGR_SHUTDOWN)