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
# 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, cc, db_file, slave_socket, module_cc_session):
107
        self._cc = cc
108
        self._mccs = module_cc_session
109
        self._check_sock = slave_socket
110
        self._db_file = db_file
111
        self._zonemgr_refresh_info = {}
112
113
114
115
116
        self._lowerbound_refresh = None
        self._lowerbound_retry = None
        self._max_transfer_timeout = None
        self._refresh_jitter = None
        self._reload_jitter = None
117
118
        self.update_config_data(module_cc_session.get_full_config(),
                                module_cc_session)
119
        self._running = False
120

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

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

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

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

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

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

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

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

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

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

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

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

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

        return False

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

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

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

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

        return None

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

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

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

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

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

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

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

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

    def _send_command(self, module_name, command_name, params):
280
        """Send command between modules."""
281
        try:
282
283
            self._mccs.rpc_call(command_name, module_name, params=params)
        except socket.error: # FIXME: WTF?
284
            logger.error(ZONEMGR_SEND_FAIL, module_name)
285
286
        except (isc.cc.session.SessionTimeout, isc.config.RPCError):
            pass        # for now we just ignore the failure
287

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

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

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

311
312
        return zone_need_refresh

313

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

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

        return False

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

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

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

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

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

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

        # Return the thread to anyone interested
        return self._thread

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

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

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

448
449
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
450

451
452
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
453

454
455
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
456

457
458
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
459

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

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

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

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

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

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

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

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

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

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

588
589
        return answer

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

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

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

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

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

Jerry's avatar
Jerry committed
618
        return ((zone_name, zone_class), master_str)
619
620
621


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

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

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

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

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

        return answer

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

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

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
701
702
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

703
    except isc.cc.session.SessionError as e:
704
705
        logger.error(ZONEMGR_SESSION_ERROR)

706
    except isc.cc.session.SessionTimeout as e:
707
708
        logger.error(ZONEMGR_SESSION_TIMEOUT)

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

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

715
    logger.info(ZONEMGR_SHUTDOWN)