#!/usr/bin/python3.4

# Copyright (C) 2010-2013  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.

"""
This file implements the Secondary Manager program.

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

import sys; sys.path.append ('/usr/lib64/python3.4/site-packages')
import os
import time
import signal
import bundy
import bundy.dns
import random
import threading
import select
import socket
import errno
from optparse import OptionParser, OptionValueError
from bundy.config.ccsession import *
import bundy.util.process
import bundy.util.traceback_handler
from bundy.log_messages.zonemgr_messages import *
from bundy.notify import notify_out
from bundy.server_common.datasrc_clients_mgr import DataSrcClientsMgr, ConfigError
from bundy.datasrc import DataSourceClient, ZoneFinder
from bundy.dns import *

# Initialize logging for called modules.
bundy.log.init("bundy-zonemgr", buffer=True)
logger = bundy.log.Logger("zonemgr")

# 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

# Constants for debug levels.
DBG_START_SHUT = logger.DBGLVL_START_SHUT
DBG_ZONEMGR_COMMAND = logger.DBGLVL_COMMAND
DBG_ZONEMGR_BASIC = logger.DBGLVL_TRACE_BASIC

bundy.util.process.rename()

# If BUNDY_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 "BUNDY_FROM_BUILD" in os.environ:
    SPECFILE_PATH = os.environ["BUNDY_FROM_BUILD"] + "/src/bin/zonemgr"
    AUTH_SPECFILE_PATH = os.environ["BUNDY_FROM_BUILD"] + "/src/bin/auth"
else:
    PREFIX = "/usr"
    DATAROOTDIR = "${prefix}/share"
    SPECFILE_PATH = "/usr/share/bundy".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__ = "BUNDY"

# define module name
XFRIN_MODULE_NAME = 'Xfrin'

# define command name
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
ZONE_NOTIFY_COMMAND = 'notify'

# define zone state
ZONE_OK = 0
ZONE_REFRESHING = 1
ZONE_EXPIRED = 2

# offsets of fields in the SOA RDATA
REFRESH_OFFSET = 3
RETRY_OFFSET = 4
EXPIRED_OFFSET = 5

class ZonemgrException(Exception):
    pass

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

    def __init__(self, slave_socket, module_cc):
        self._module_cc = module_cc
        self._check_sock = slave_socket
        self._zonemgr_refresh_info = {}
        self._lowerbound_refresh = None
        self._lowerbound_retry = None
        self._max_transfer_timeout = None
        self._refresh_jitter = None
        self._reload_jitter = None
        # This is essentially private, but we allow tests to customize it.
        self._datasrc_clients_mgr = DataSrcClientsMgr()
        # data_sources configuration should be ready with cfgmgr, so this
        # shouldn't fail; if it ever does we simply propagate the exception
        # to terminate the program.
        self._module_cc.add_remote_config_by_name('data_sources',
                                                  self._datasrc_config_handler)
        self.update_config_data(module_cc.get_full_config(), module_cc)
        self._running = False

    def _random_jitter(self, max, jitter):
        """Imposes some random jitters for refresh and
        retry timers to avoid many zones need to do refresh
        at the same time.
        The value should be between (max - jitter) and max.
        """
        if 0 == jitter:
            return max
        return random.uniform(max - jitter, max)

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

    def _set_zone_timer(self, zone_name_class, max, jitter):
        """Set zone next refresh time.
        jitter should not be bigger than half the original value."""
        self._set_zone_next_refresh_time(zone_name_class,
                                         self._get_current_time() +
                                            self._random_jitter(max, jitter))

    def _set_zone_refresh_timer(self, zone_name_class):
        """Set zone next refresh time after zone refresh success.
           now + refresh - refresh_jitter <= next_refresh_time <= now + refresh
           """
        zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).
                                      split(" ")[REFRESH_OFFSET])
        zone_refresh_time = max(self._lowerbound_refresh, zone_refresh_time)
        self._set_zone_timer(zone_name_class, zone_refresh_time,
                             self._refresh_jitter * zone_refresh_time)

    def _set_zone_retry_timer(self, zone_name_class):
        """Set zone next refresh time after zone refresh fail.
           now + retry - retry_jitter <= next_refresh_time <= now + retry
           """
        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
        zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
        self._set_zone_timer(zone_name_class, zone_retry_time,
                             self._refresh_jitter * zone_retry_time)

    def _set_zone_notify_timer(self, zone_name_class):
        """Set zone next refresh time after receiving notify
           next_refresh_time = now
        """
        self._set_zone_timer(zone_name_class, 0, 0)

    def _zone_not_exist(self, zone_name_class):
        """ Zone doesn't belong to zonemgr"""
        return not zone_name_class in self._zonemgr_refresh_info

    def zone_refresh_success(self, zone_name_class):
        """Update zone info after zone refresh success"""
        if self._zone_not_exist(zone_name_class):
            logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0],
                         zone_name_class[1])
            raise ZonemgrException("[bundy-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
        self.zonemgr_reload_zone(zone_name_class)
        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())

    def zone_refresh_fail(self, zone_name_class):
        """Update zone info after zone refresh fail"""
        if self._zone_not_exist(zone_name_class):
            logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0],
                         zone_name_class[1])
            raise ZonemgrException("[bundy-zonemgr] Zone (%s, %s) doesn't "
                                   "belong to zonemgr" % zone_name_class)
        # Is zone expired?
        if ((self._get_zone_soa_rdata(zone_name_class) is None) or
            self._zone_is_expired(zone_name_class)):
            self._set_zone_state(zone_name_class, ZONE_EXPIRED)
        else:
            self._set_zone_state(zone_name_class, ZONE_OK)
        self._set_zone_retry_timer(zone_name_class)

    def zone_handle_notify(self, zone_name_class, master):
        """Handle an incoming 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.

        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):
            logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_ZONE_NOTIFY_NOT_SECONDARY,
                         zone_name_class[0], zone_name_class[1], master)
            return False
        self._set_zone_notifier_master(zone_name_class, master)
        self._set_zone_notify_timer(zone_name_class)
        return True

    def zonemgr_reload_zone(self, zone_name_class):
        """ Reload a zone."""
        self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = \
            self._get_zone_soa(zone_name_class)

    def zonemgr_add_zone(self, zone_name_class):
        """ Add a zone into zone manager."""
        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0],
                     zone_name_class[1])
        zone_info = {}
        zone_soa = self._get_zone_soa(zone_name_class)
        if zone_soa is None:
            logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
            zone_info["zone_soa_rdata"] = None
            zone_reload_time = 0.0
        else:
            zone_info["zone_soa_rdata"] = zone_soa
            zone_reload_time = float(zone_soa.split(" ")[RETRY_OFFSET])
        zone_info["zone_state"] = ZONE_OK
        zone_info["last_refresh_time"] = self._get_current_time()
        self._zonemgr_refresh_info[zone_name_class] = zone_info
        # Imposes some random jitters to avoid many zones need to do refresh
        # at the same time.
        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)

    def _get_zone_soa(self, zone_name_class):
        """Retrieve the current SOA RR of the zone to be transferred."""

        def get_zone_soa_rrset(datasrc_client, zone_name, zone_class):
            """Retrieve the current SOA RR of the zone to be transferred."""
            # get the zone finder.  this must be SUCCESS (not even
            # PARTIALMATCH) because we are specifying the zone origin name.
            result, finder = datasrc_client.find_zone(zone_name)
            if result != DataSourceClient.SUCCESS:
                # The data source doesn't know the zone.  In the context in
                # which this function is called, this shouldn't happen.
                raise ZonemgrException(
                    "unexpected result: zone %s/%s doesn't exist" %
                    (zone_name.to_text(True), str(zone_class)))
            result, soa_rrset, _ = finder.find(zone_name, RRType.SOA)
            if result != ZoneFinder.SUCCESS:
                logger.warn(ZONEMGR_NO_SOA,
                            zone_name.to_text(True), str(zone_class))
                return None
            return soa_rrset

        # Identify the data source to which the zone content is transferred,
        # and get the current zone SOA from the data source (if available).
        datasrc_client = None
        clist = self._datasrc_clients_mgr.get_client_list(zone_name_class[1])
        if clist is None:
            return None
        try:
            datasrc_client = clist.find(zone_name_class[0], True, False)[0]
            if datasrc_client is None: # can happen, so log it separately.
                logger.error(ZONEMGR_DATASRC_UNKNOWN,
                             zone_name_class[0] + '/' + zone_name_class[1])
                return None
            zone_soa = get_zone_soa_rrset(datasrc_client,
                                          Name(zone_name_class[0]),
                                          RRClass(zone_name_class[1]))
            if zone_soa == None:
                return None
            else:
                return zone_soa.get_rdata()[0].to_text()
        except bundy.datasrc.Error as ex:
            # rare case error. re-raise as ZonemgrException so it'll be logged
            # in command_handler().
            raise ZonemgrException('unexpected failure in datasrc module: ' +
                                   str(ex))

    def _zone_is_expired(self, zone_name_class):
        """Judge whether a zone is expired or not."""
        zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).
                                      split(" ")[EXPIRED_OFFSET])
        zone_last_refresh_time = \
            self._get_zone_last_refresh_time(zone_name_class)
        if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
            zone_last_refresh_time + zone_expired_time <=
                self._get_current_time()):
            return True

        return False

    def _get_zone_soa_rdata(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]

    def _get_zone_last_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]

    def _set_zone_last_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time

    def _get_zone_notifier_master(self, zone_name_class):
        if ("notify_master" in
                self._zonemgr_refresh_info[zone_name_class].keys()):
            return self._zonemgr_refresh_info[zone_name_class]["notify_master"]

        return None

    def _set_zone_notifier_master(self, zone_name_class, master_addr):
        self._zonemgr_refresh_info[zone_name_class]["notify_master"] = \
            master_addr

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

    def _get_zone_state(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["zone_state"]

    def _set_zone_state(self, zone_name_class, zone_state):
        self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state

    def _get_zone_refresh_timeout(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]

    def _set_zone_refresh_timeout(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time

    def _get_zone_next_refresh_time(self, zone_name_class):
        return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]

    def _set_zone_next_refresh_time(self, zone_name_class, time):
        self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time

    def _send_command(self, module_name, command_name, params):
        """Send command between modules."""
        try:
            self._module_cc.rpc_call(command_name, module_name, params=params)
        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.
            logger.error(ZONEMGR_SEND_FAIL, module_name)
        except (bundy.cc.session.SessionTimeout, bundy.config.RPCError):
            pass        # for now we just ignore the failure

    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)
            # If hasn't received refresh response but are within refresh
            # timeout, skip the zone
            if (ZONE_REFRESHING == zone_state and
                (self._get_zone_refresh_timeout(zone_name_class) >
                     self._get_current_time())):
                continue

            # Get the zone with minimum next_refresh_time
            if ((zone_need_refresh is None) or
                (self._get_zone_next_refresh_time(zone_name_class) <
                 self._get_zone_next_refresh_time(zone_need_refresh))):
                zone_need_refresh = zone_name_class

            # Find the zone need do refresh
            if (self._get_zone_next_refresh_time(zone_need_refresh) <
                    self._get_current_time()):
                break

        return zone_need_refresh


    def _do_refresh(self, zone_name_class):
        """Do zone refresh."""
        logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE,
                     zone_name_class[0], zone_name_class[1])
        self._set_zone_state(zone_name_class, ZONE_REFRESHING)
        self._set_zone_refresh_timeout(zone_name_class,
                                       self._get_current_time() +
                                           self._max_transfer_timeout)
        notify_master = self._get_zone_notifier_master(zone_name_class)
        # If the zone has notify master, send notify command to xfrin module
        if notify_master:
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1],
                     "master" : notify_master
                     }
            self._send_command(XFRIN_MODULE_NAME, ZONE_NOTIFY_COMMAND, param)
            self._clear_zone_notifier_master(zone_name_class)
        # Send refresh command to xfrin module
        else:
            param = {"zone_name" : zone_name_class[0],
                     "zone_class" : zone_name_class[1]
                    }
            self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)

    def _run_timer(self, start_event):
        while self._running:
            # 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
            # a race condition and _running could be set to false before we
            # could enter it
            if start_event:
                start_event.set()
                start_event = None
            # If zonemgr has no zone, set timeout to minimum
            if not self._zonemgr_refresh_info:
                timeout = self._lowerbound_retry
            else:
                zone_need_refresh = self._find_need_do_refresh_zone()
                # If don't get zone with minimum next refresh time, set
                # timeout to minimum
                if not zone_need_refresh:
                    timeout = self._lowerbound_retry
                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

            """ Wait for the socket notification for a maximum time of timeout
            in seconds (as float)."""
            try:
                rlist, wlist, xlist = \
                    select.select([self._check_sock, self._read_sock],
                                  [], [], timeout)
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                else:
                    logger.error(ZONEMGR_SELECT_ERROR, e);
                    break

            for fd in rlist:
                if fd == self._read_sock: # awaken by shutdown socket
                    # self._running will be False by now, if it is not a false
                    # alarm (linux kernel is said to trigger spurious wakeup
                    # on a filehandle that is not really readable).
                    continue
                if fd == self._check_sock: # awaken by check socket
                    self._check_sock.recv(32)

    def run_timer(self, daemon=False):
        """
        Keep track of zone timers. Spawns and starts a thread. The thread
        object is returned.

        You can stop it by calling shutdown().
        """
        # Small sanity check
        if self._running:
            logger.error(ZONEMGR_TIMER_THREAD_RUNNING)
            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()
        start_event = threading.Event()

        # Start the thread
        self._thread = threading.Thread(target = self._run_timer,
            args = (start_event,))
        if daemon:
            self._thread.setDaemon(True)
        self._thread.start()
        start_event.wait()

        # Return the thread to anyone interested
        return self._thread

    def _datasrc_config_handler(self, new_config, config_data):
        """Configuration handler of the 'data_sources' module.

        The actual handling is delegated to the DataSrcClientsMgr class;
        this method is a simple wrapper.

        This is essentially private, but implemented as 'protected' so tests
        can refer to it; other external use is prohibited.
        """
        try:
            self._datasrc_clients_mgr.reconfigure(new_config, config_data)
        except bundy.server_common.datasrc_clients_mgr.ConfigError as ex:
            logger.error(ZONEMGR_DATASRC_CONFIG_ERROR, ex)

    def shutdown(self):
        """
        Stop the run_timer() thread. Block until it finished. This must be
        called from a different thread.
        """
        if not self._running:
            logger.error(ZONEMGR_NO_TIMER_THREAD)
            raise RuntimeError("Trying to shutdown, but not running")

        # Ask the thread to stop
        self._running = False
        self._write_sock.send(b'shutdown') # make self._read_sock readable
        # Wait for it to actually finnish
        self._thread.join()
        # Wipe out what we do not need
        self._thread = None
        self._read_sock.close()
        self._write_sock.close()
        self._read_sock = None
        self._write_sock = None

    def update_config_data(self, new_config, module_cc):
        """ update ZonemgrRefresh config """
        # Get a new value, but only if it is defined (commonly used below)
        # 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)

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

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

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

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

        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'):
                    if 'name' not in secondary_zone:
                        raise ZonemgrException("Secondary zone specified "
                                               "without a name")
                    name = secondary_zone['name']

                    # Convert to Name and back (both to check and to normalize)
                    try:
                        name = bundy.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))

                    # Currently we use an explicit get_default_value call
                    # in case the class hasn't been set. Alternatively, we
                    # could use
                    # module_cc.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:
                        rr_class = secondary_zone['class']
                    else:
                        rr_class = module_cc.get_default_value(
                                        'secondary_zones/class')
                    # Convert rr_class to and from RRClass to check its value
                    try:
                        name_class = (name, bundy.dns.RRClass(rr_class).to_text())
                    except bundy.dns.InvalidRRClass:
                        raise ZonemgrException("Bad RR class '" +
                                               rr_class +
                                               "' for zone " + name)
                    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

class Zonemgr:
    """Zone manager class."""
    def __init__(self):
        self._zone_refresh = None
        self._setup_session()
        # Create socket pair for communicating between main thread and zonemgr
        # timer thread
        self._master_socket, self._slave_socket = \
            socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
        self._zone_refresh = ZonemgrRefresh(self._slave_socket, self._module_cc)
        self._zone_refresh.run_timer()

        self._lock = threading.Lock()
        self._shutdown_event = threading.Event()
        self.running = False

    def _setup_session(self):
        """Setup two sessions for zonemgr, one(self._module_cc) is used for
        receiving commands and config data sent from other modules, another
        one (self._cc) is used to send commands to proper modules.
        """
        self._module_cc = bundy.config.ModuleCCSession(SPECFILE_LOCATION,
                                                  self.config_handler,
                                                  self.command_handler)
        self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
        self._config_data = self._module_cc.get_full_config()
        self._config_data_check(self._config_data)
        self._module_cc.start()

    def shutdown(self):
        """Shutdown the zonemgr process. The thread which is keeping track of
        zone timers should be terminated.
        """
        self._zone_refresh.shutdown()

        self._slave_socket.close()
        self._master_socket.close()
        self._shutdown_event.set()
        self.running = False

    def config_handler(self, new_config):
        """ Update config data. """
        answer = create_answer(0)
        ok = True
        complete = self._config_data.copy()
        for key in new_config:
            if key not in complete:
                answer = create_answer(1, "Unknown config data: " + str(key))
                ok = False
                continue
            complete[key] = new_config[key]

        self._config_data_check(complete)
        if self._zone_refresh is not None:
            try:
                self._zone_refresh.update_config_data(complete,
                                                      self._module_cc)
            except Exception as e:
                answer = create_answer(1, str(e))
                ok = False
        if ok:
            self._config_data = complete

        return answer

    def _config_data_check(self, config_data):
        """Check whether the new config data is valid or
        not. It contains only basic logic, not full check against
        database.
        """
        # jitter should not be bigger than half of the original value
        if config_data.get('refresh_jitter') > 0.5:
            config_data['refresh_jitter'] = 0.5
            logger.warn(ZONEMGR_JITTER_TOO_BIG)

    def _parse_cmd_params(self, args, command):
        zone_name = args.get("zone_name")
        if not zone_name:
            logger.error(ZONEMGR_NO_ZONE_NAME)
            raise ZonemgrException("zone name should be provided")

        zone_class = args.get("zone_class")
        if not zone_class:
            logger.error(ZONEMGR_NO_ZONE_CLASS)
            raise ZonemgrException("zone class should be provided")

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

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

        return ((zone_name, zone_class), master_str)


    def command_handler(self, command, args):
        """Handle command receivd from command channel.
        ZONE_NOTIFY_COMMAND is issued by Auth process;
        ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
        Xfrin process;
        shutdown is issued by a user or Init process.
        """
        answer = create_answer(0)
        if command == ZONE_NOTIFY_COMMAND:
            """ Handle Auth notify command"""
            # master is the source sender of the notify message.
            zone_name_class, master = self._parse_cmd_params(args, command)
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY,
                         zone_name_class[0], zone_name_class[1])
            with self._lock:
                need_refresh = self._zone_refresh.zone_handle_notify(
                    zone_name_class, master)
            if need_refresh:
                # Send notification to zonemgr timer thread by making
                # self._slave_socket readable.
                self._master_socket.send(b" ")

        elif command == notify_out.ZONE_NEW_DATA_READY_CMD:
            """ Handle xfrin success command"""
            zone_name_class = self._parse_cmd_params(args, command)
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS,
                         zone_name_class[0], zone_name_class[1])
            with self._lock:
                self._zone_refresh.zone_refresh_success(zone_name_class)
            self._master_socket.send(b" ")# make self._slave_socket readable

        elif command == notify_out.ZONE_XFRIN_FAILED:
            """ Handle xfrin fail command"""
            zone_name_class = self._parse_cmd_params(args, command)
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED,
                         zone_name_class[0], zone_name_class[1])
            with self._lock:
                self._zone_refresh.zone_refresh_fail(zone_name_class)
            self._master_socket.send(b" ")# make self._slave_socket readable

        elif command == "shutdown":
            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)
            self.shutdown()

        else:
            logger.warn(ZONEMGR_RECEIVE_UNKNOWN, str(command))
            answer = create_answer(1, "Unknown command:" + str(command))

        return answer

    def run(self):
        logger.debug(DBG_PROCESS, ZONEMGR_STARTED)
        self.running = True
        try:
            while not self._shutdown_event.is_set():
                fileno = self._module_cc.get_socket().fileno()
                reads = []
                # 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)
        finally:
            self._module_cc.send_stopping()

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

def main():
    try:
        logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
        if options.verbose:
            logger.set_severity("DEBUG", 99)

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

    except bundy.cc.session.SessionError as e:
        logger.error(ZONEMGR_SESSION_ERROR)

    except bundy.cc.session.SessionTimeout as e:
        logger.error(ZONEMGR_SESSION_TIMEOUT)

    except bundy.config.ModuleCCSessionError as e:
        logger.error(ZONEMGR_CCSESSION_ERROR, str(e))

    if zonemgrd and zonemgrd.running:
        zonemgrd.shutdown()

    logger.info(ZONEMGR_SHUTDOWN)

if '__main__' == __name__:
    bundy.util.traceback_handler.traceback_handler(main)
