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

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

18
"""
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
This file implements the Secondary Manager program.

The secondary manager is one of the co-operating processes
of BIND10, which keeps track of timers and other information
necessary for BIND10 to act as a slave.
"""

import sys; sys.path.append ('@@PYTHONPATH@@')
import os
import time
import signal
import isc
import random
import threading
import select
import socket
35
import errno
36
37
38
from isc.datasrc import sqlite3_ds
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
39
import isc.util.process
40
from isc.log_messages.zonemgr_messages import *
Michal Vaner's avatar
Michal Vaner committed
41

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

# Constants for debug levels, to be removed when we have #1074.
DBG_START_SHUT = 0
DBG_ZONEMGR_COMMAND = 10
DBG_ZONEMGR_BASIC = 40
50

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

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

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

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

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

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

90
91
92
class ZonemgrException(Exception):
    pass

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

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

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

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

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

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

Jerry's avatar
Jerry committed
141
    def _set_zone_retry_timer(self, zone_name_class):
142
        """Set zone next refresh time after zone refresh fail.
143
           now + retry - retry_jitter <= next_refresh_time <= now + retry
144
           """
145
146
147
148
        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
149
        zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
150
        self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
151

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

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

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

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

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

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

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

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

        return False

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

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

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

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

        return None

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

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

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

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

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

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

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

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

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

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

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

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

306
307
        return zone_need_refresh

308

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

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

        return False

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

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

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

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

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

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

        # Return the thread to anyone interested
        return self._thread

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

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

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

441
442
        self._lowerbound_retry = val_or_default(
            new_config.get('lowerbound_retry'), self._lowerbound_retry)
443

444
445
        self._max_transfer_timeout = val_or_default(
            new_config.get('max_transfer_timeout'), self._max_transfer_timeout)
446

447
448
        self._refresh_jitter = val_or_default(
            new_config.get('refresh_jitter'), self._refresh_jitter)
449

450
451
        self._reload_jitter = val_or_default(
            new_config.get('reload_jitter'), self._reload_jitter)
452

453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
        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'):
                    name = secondary_zone['name']
                    # Be tolerant to sclerotic users who forget the final dot
                    if name[-1] != '.':
                        name = name + '.'
                    name_class = (name, secondary_zone['class'])
                    required[name_class] = True
                    # Add it only if it isn't there already
                    if not name_class in self._zonemgr_refresh_info:
                        # 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
479

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

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

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

    def get_db_file(self):
Jerry's avatar
Jerry committed
509
        db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
510
511
512
        # this too should be unnecessary, but currently the
        # 'from build' override isn't stored in the config
        # (and we don't have indirect python access to datasources yet)
513
514
515
516
517
        if is_default and "B10_FROM_BUILD" in os.environ:
            db_file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
        return db_file

    def shutdown(self):
518
519
        """Shutdown the zonemgr process. The thread which is keeping track of
           zone timers should be terminated.
520
        """
Jerry's avatar
Jerry committed
521
522
        self._zone_refresh.shutdown()

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

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

540
        self._config_data_check(complete)
541
        if self._zone_refresh is not None:
542
543
544
545
546
547
548
            try:
                self._zone_refresh.update_config_data(complete)
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete
549

550
551
        return answer

Jerry's avatar
Jerry committed
552
    def _config_data_check(self, config_data):
553
554
555
        """Check whether the new config data is valid or
        not. It contains only basic logic, not full check against
        database."""
Jerry's avatar
Jerry committed
556
        # jitter should not be bigger than half of the original value
557
558
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
559
            logger.warn(ZONEMGR_JITTER_TOO_BIG)
Jerry's avatar
Jerry committed
560

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

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

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

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

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


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

Jerry's avatar
Jerry committed
600
        elif command == ZONE_XFRIN_SUCCESS_COMMAND:
601
            """ Handle xfrin success command"""
Jerry's avatar
Jerry committed
602
            zone_name_class = self._parse_cmd_params(args, command)
603
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
604
            with self._lock:
605
                self._zone_refresh.zone_refresh_success(zone_name_class)
Jerry's avatar
Jerry committed
606
            self._master_socket.send(b" ")# make self._slave_socket readble
607

Jerry's avatar
Jerry committed
608
        elif command == ZONE_XFRIN_FAILED_COMMAND:
Jerry's avatar
Jerry committed
609
            """ Handle xfrin fail command"""
Jerry's avatar
Jerry committed
610
            zone_name_class = self._parse_cmd_params(args, command)
611
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])
612
            with self._lock:
613
                self._zone_refresh.zone_refresh_fail(zone_name_class)
Jerry's avatar
Jerry committed
614
            self._master_socket.send(b" ")# make self._slave_socket readble
615
616

        elif command == "shutdown":
617
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
618
619
620
            self.shutdown()

        else:
621
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
622
623
624
625
626
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
Michal Vaner's avatar
Michal Vaner committed
627
        self.running = True
628
        while not self._shutdown_event.is_set():
629
            self._module_cc.check_command(False)
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647

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:
648
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
649
650
651
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
652
        if options.verbose:
653
            logger.set_severity("DEBUG", 99)
654
655
656
657
658

        set_signal_handler()
        zonemgrd = Zonemgr()
        zonemgrd.run()
    except KeyboardInterrupt:
659
660
        logger.info(ZONEMGR_KEYBOARD_INTERRUPT)

661
    except isc.cc.session.SessionError as e:
662
663
        logger.error(ZONEMGR_SESSION_ERROR)

664
    except isc.cc.session.SessionTimeout as e:
665
666
        logger.error(ZONEMGR_SESSION_TIMEOUT)

667
    except isc.config.ModuleCCSessionError as e:
668
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))
669

Michal Vaner's avatar
Michal Vaner committed
670
    if zonemgrd and zonemgrd.running:
671
672
        zonemgrd.shutdown()

673
    logger.debug(DBG_START_SHUT, ZONEMGR_SHUTDOWN)