xfrout.py.in 21.7 KB
Newer Older
1 2 3
#!@PYTHON@

# Copyright (C) 2010  Internet Systems Consortium.
4
# Copyright (C) 2010  CZ NIC
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#
# 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.


import sys; sys.path.append ('@@PYTHONPATH@@')
import isc
import isc.cc
import threading
import struct
import signal
Evan Hunt's avatar
Evan Hunt committed
26
from isc.datasrc import sqlite3_ds
27 28 29
from socketserver import *
import os
from isc.config.ccsession import *
30
from isc.log.log import *
31
from isc.cc import SessionError, SessionTimeout
32
from isc.notify import notify_out
33
import isc.util.process
34
import socket
35
import select
36
import errno
37
from optparse import OptionParser, OptionValueError
Likun Zhang's avatar
Likun Zhang committed
38
from isc.util import socketserver_mixin
39

40
try:
41
    from libxfr_python import *
Jelte Jansen's avatar
Jelte Jansen committed
42
    from pydnspp import *
43 44 45 46
except ImportError as e:
    # C++ loadable module may not be installed; even so the xfrout process
    # must keep running, so we warn about it and move forward.
    sys.stderr.write('[b10-xfrout] failed to import DNS or XFR module: %s\n' % str(e))
Michal Vaner's avatar
Michal Vaner committed
47

48
isc.util.process.rename()
49

50 51
if "B10_FROM_BUILD" in os.environ:
    SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/xfrout"
52
    AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
53
    UNIX_SOCKET_FILE= os.environ["B10_FROM_BUILD"] + "/auth_xfrout_conn"
54 55 56 57
else:
    PREFIX = "@prefix@"
    DATAROOTDIR = "@datarootdir@"
    SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
58
    AUTH_SPECFILE_PATH = SPECFILE_PATH
59
    UNIX_SOCKET_FILE = "@@LOCALSTATEDIR@@/auth_xfrout_conn"
60

61
SPECFILE_LOCATION = SPECFILE_PATH + "/xfrout.spec"
62
AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + os.sep + "auth.spec"
63
MAX_TRANSFERS_OUT = 10
Jerry's avatar
Jerry committed
64
VERBOSE_MODE = False
65

66

67 68 69 70 71 72 73 74 75
XFROUT_MAX_MESSAGE_SIZE = 65535

def get_rrset_len(rrset):
    """Returns the wire length of the given RRset"""
    bytes = bytearray()
    rrset.to_wire(bytes)
    return len(bytes)


76
class XfroutSession(BaseRequestHandler):
77
    def __init__(self, request, client_address, server, log, sock):
Jelte Jansen's avatar
Jelte Jansen committed
78 79
        # The initializer for the superclass may call functions
        # that need _log to be set, so we set it first
Jerry's avatar
Jerry committed
80
        self._log = log
81
        self._shutdown_sock = sock
Jelte Jansen's avatar
Jelte Jansen committed
82
        BaseRequestHandler.__init__(self, request, client_address, server)
Jerry's avatar
Jerry committed
83

84
    def handle(self):
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
        '''Handle a request until shutdown or xfrout client is closed.'''
        # check self.server._shutdown_event to ensure the real shutdown comes.
        # Linux could trigger a spurious readable event on the _shutdown_sock 
        # due to a bug, so we need perform a double check. 
        while not self.server._shutdown_event.is_set(): # Check if xfrout is shutdown
            try:
                (rlist, wlist, xlist) = select.select([self._shutdown_sock, self.request], [], [])
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    (rlist, wlist, xlist) = ([], [], [])
                    continue
                else:
                    self._log.log_message("error", "Error with select(): %s" %e)
                    break
            # self.server._shutdown_evnet will be set by now, if it is not a false
            # alarm
            if self._shutdown_sock in rlist:
                continue
103

104 105 106 107 108 109 110 111 112
            sock_fd = recv_fd(self.request.fileno())

            if sock_fd < 0:
                # This may happen when one xfrout process try to connect to
                # xfrout unix socket server, to check whether there is another
                # xfrout running.
                if sock_fd == XFR_FD_RECEIVE_FAIL:
                    self._log.log_message("error", "Failed to receive the file descriptor for XFR connection")
                break
113

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
            # receive query msg
            msgdata = self._receive_query_message(self.request)
            if not msgdata:
                break

            try:
                self.dns_xfrout_start(sock_fd, msgdata)
                #TODO, avoid catching all exceptions
            except Exception as e:
                self._log.log_message("error", str(e))

            os.close(sock_fd)

    def _receive_query_message(self, sock):
        ''' receive query message from sock'''
        # receive data length
        data_len = sock.recv(2)
        if not data_len:
            return None
        msg_len = struct.unpack('!H', data_len)[0]
        # receive data
        recv_size = 0
        msgdata = b''
        while recv_size < msg_len:
            data = sock.recv(msg_len - recv_size)
            if not data:
                return None
            recv_size += len(data)
            msgdata += data

        return msgdata
145

146 147
    def _parse_query_message(self, mdata):
        ''' parse query message to [socket,message]'''
148
        #TODO, need to add parseHeader() in case the message header is invalid
149
        try:
150
            msg = Message(Message.PARSE)
151
            Message.from_wire(msg, mdata)
152
        except Exception as err:
Jerry's avatar
Jerry committed
153
            self._log.log_message("error", str(err))
154
            return Rcode.FORMERR(), None
155

156
        return Rcode.NOERROR(), msg
157 158

    def _get_query_zone_name(self, msg):
159
        question = msg.get_question()[0]
160 161 162
        return question.get_name().to_text()


163
    def _send_data(self, sock_fd, data):
164 165 166
        size = len(data)
        total_count = 0
        while total_count < size:
167
            count = os.write(sock_fd, data[total_count:])
168 169 170
            total_count += count


171
    def _send_message(self, sock_fd, msg):
172
        render = MessageRenderer()
173 174
        # As defined in RFC5936 section3.4, perform case-preserving name
        # compression for AXFR message.
175
        render.set_compress_mode(MessageRenderer.CASE_SENSITIVE)
176
        render.set_length_limit(XFROUT_MAX_MESSAGE_SIZE)
177
        msg.to_wire(render)
178
        header_len = struct.pack('H', socket.htons(render.get_length()))
179 180
        self._send_data(sock_fd, header_len)
        self._send_data(sock_fd, render.get_data())
181 182


183
    def _reply_query_with_error_rcode(self, msg, sock_fd, rcode_):
184 185
        msg.make_response()
        msg.set_rcode(rcode_)
186
        self._send_message(sock_fd, msg)
187 188


189
    def _reply_query_with_format_error(self, msg, sock_fd):
190 191
        '''query message format isn't legal.'''
        if not msg:
192
            return # query message is invalid. send nothing back.
193 194

        msg.make_response()
195
        msg.set_rcode(Rcode.FORMERR())
196
        self._send_message(sock_fd, msg)
197 198


JINMEI Tatuya's avatar
JINMEI Tatuya committed
199 200 201 202 203 204 205
    def _zone_has_soa(self, zone):
        '''Judge if the zone has an SOA record.'''
        # In some sense, the SOA defines a zone.
        # If the current name server has authority for the
        # specific zone, we need to judge if the zone has an SOA record;
        # if not, we consider the zone has incomplete data, so xfrout can't
        # serve for it.
206
        if sqlite3_ds.get_zone_soa(zone, self.server.get_db_file()):
JINMEI Tatuya's avatar
JINMEI Tatuya committed
207
            return True
JINMEI Tatuya's avatar
JINMEI Tatuya committed
208

JINMEI Tatuya's avatar
JINMEI Tatuya committed
209 210
        return False

JINMEI Tatuya's avatar
JINMEI Tatuya committed
211 212 213 214 215 216 217 218
    def _zone_exist(self, zonename):
        '''Judge if the zone is configured by config manager.'''
        # Currently, if we find the zone in datasource successfully, we
        # consider the zone is configured, and the current name server has
        # authority for the specific zone.
        # TODO: should get zone's configuration from cfgmgr or other place
        # in future.
        return sqlite3_ds.zone_exist(zonename, self.server.get_db_file())
219

220 221 222
    def _check_xfrout_available(self, zone_name):
        '''Check if xfr request can be responsed.
           TODO, Get zone's configuration from cfgmgr or some other place
223
           eg. check allow_transfer setting,
224
        '''
JINMEI Tatuya's avatar
JINMEI Tatuya committed
225 226
        # If the current name server does not have authority for the
        # zone, xfrout can't serve for it, return rcode NOTAUTH.
227
        if not self._zone_exist(zone_name):
228
            return Rcode.NOTAUTH()
229

JINMEI Tatuya's avatar
JINMEI Tatuya committed
230 231 232 233
        # If we are an authoritative name server for the zone, but fail
        # to find the zone's SOA record in datasource, xfrout can't
        # provide zone transfer for it.
        if not self._zone_has_soa(zone_name):
234
            return Rcode.SERVFAIL()
235 236 237

        #TODO, check allow_transfer
        if not self.server.increase_transfers_counter():
238
            return Rcode.REFUSED()
239

240
        return Rcode.NOERROR()
241 242


243
    def dns_xfrout_start(self, sock_fd, msg_query):
244 245
        rcode_, msg = self._parse_query_message(msg_query)
        #TODO. create query message and parse header
246
        if rcode_ != Rcode.NOERROR():
247
            return self._reply_query_with_format_error(msg, sock_fd)
248 249 250

        zone_name = self._get_query_zone_name(msg)
        rcode_ = self._check_xfrout_available(zone_name)
251
        if rcode_ != Rcode.NOERROR():
252 253
            self._log.log_message("info", "transfer of '%s/IN' failed: %s",
                                  zone_name, rcode_.to_text())
254
            return self. _reply_query_with_error_rcode(msg, sock_fd, rcode_)
255 256

        try:
Jerry's avatar
Jerry committed
257
            self._log.log_message("info", "transfer of '%s/IN': AXFR started" % zone_name)
258
            self._reply_xfrout_query(msg, sock_fd, zone_name)
Jerry's avatar
Jerry committed
259
            self._log.log_message("info", "transfer of '%s/IN': AXFR end" % zone_name)
260
        except Exception as err:
Jerry's avatar
Jerry committed
261
            self._log.log_message("error", str(err))
262 263

        self.server.decrease_transfers_counter()
264
        return
265 266 267 268 269 270


    def _clear_message(self, msg):
        qid = msg.get_qid()
        opcode = msg.get_opcode()
        rcode = msg.get_rcode()
271

272
        msg.clear(Message.RENDER)
273 274 275
        msg.set_qid(qid)
        msg.set_opcode(opcode)
        msg.set_rcode(rcode)
276 277
        msg.set_header_flag(Message.HEADERFLAG_AA)
        msg.set_header_flag(Message.HEADERFLAG_QR)
278 279 280
        return msg

    def _create_rrset_from_db_record(self, record):
281
        '''Create one rrset from one record of datasource, if the schema of record is changed,
282 283
        This function should be updated first.
        '''
284 285 286
        rrtype_ = RRType(record[5])
        rdata_ = Rdata(rrtype_, RRClass("IN"), " ".join(record[7:]))
        rrset_ = RRset(Name(record[2]), RRClass("IN"), rrtype_, RRTTL( int(record[4])))
287 288
        rrset_.add_rdata(rdata_)
        return rrset_
289 290

    def _send_message_with_last_soa(self, msg, sock_fd, rrset_soa, message_upper_len):
291 292 293
        '''Add the SOA record to the end of message. If it can't be
        added, a new message should be created to send out the last soa .
        '''
294
        rrset_len = get_rrset_len(rrset_soa)
295

296
        if message_upper_len + rrset_len < XFROUT_MAX_MESSAGE_SIZE:
297
            msg.add_rrset(Message.SECTION_ANSWER, rrset_soa)
298
        else:
299
            self._send_message(sock_fd, msg)
300
            msg = self._clear_message(msg)
301
            msg.add_rrset(Message.SECTION_ANSWER, rrset_soa)
302

303
        self._send_message(sock_fd, msg)
304 305


306
    def _reply_xfrout_query(self, msg, sock_fd, zone_name):
307 308
        #TODO, there should be a better way to insert rrset.
        msg.make_response()
309
        msg.set_header_flag(Message.HEADERFLAG_AA)
310 311
        soa_record = sqlite3_ds.get_zone_soa(zone_name, self.server.get_db_file())
        rrset_soa = self._create_rrset_from_db_record(soa_record)
312
        msg.add_rrset(Message.SECTION_ANSWER, rrset_soa)
313

314 315
        message_upper_len = get_rrset_len(rrset_soa)

316 317
        for rr_data in sqlite3_ds.get_zone_datas(zone_name, self.server.get_db_file()):
            if  self.server._shutdown_event.is_set(): # Check if xfrout is shutdown
318 319
                self._log.log_message("info", "xfrout process is being shutdown")
                return
320

321 322
            # TODO: RRType.SOA() ?
            if RRType(rr_data[5]) == RRType("SOA"): #ignore soa record
323
                continue
Jelte Jansen's avatar
Jelte Jansen committed
324

325
            rrset_ = self._create_rrset_from_db_record(rr_data)
326 327 328 329 330 331

            # We calculate the maximum size of the RRset (i.e. the
            # size without compression) and use that to see if we
            # may have reached the limit
            rrset_len = get_rrset_len(rrset_)
            if message_upper_len + rrset_len < XFROUT_MAX_MESSAGE_SIZE:
332
                msg.add_rrset(Message.SECTION_ANSWER, rrset_)
333
                message_upper_len += rrset_len
334 335
                continue

336
            self._send_message(sock_fd, msg)
337
            msg = self._clear_message(msg)
338
            msg.add_rrset(Message.SECTION_ANSWER, rrset_) # Add the rrset to the new message
339
            message_upper_len = rrset_len
340

341
        self._send_message_with_last_soa(msg, sock_fd, rrset_soa, message_upper_len)
342

343
class UnixSockServer(socketserver_mixin.NoPollMixIn, ThreadingUnixStreamServer):
344 345
    '''The unix domain socket server which accept xfr query sent from auth server.'''

346
    def __init__(self, sock_file, handle_class, shutdown_event, config_data, cc, log):
347
        self._remove_unused_sock_file(sock_file)
348
        self._sock_file = sock_file
349
        socketserver_mixin.NoPollMixIn.__init__(self)
350 351 352 353
        ThreadingUnixStreamServer.__init__(self, sock_file, handle_class)
        self._lock = threading.Lock()
        self._transfers_counter = 0
        self._shutdown_event = shutdown_event
354
        self._write_sock, self._read_sock = socket.socketpair()
355
        self._log = log
356
        self.update_config_data(config_data)
357
        self._cc = cc
358

Jerry's avatar
Jerry committed
359 360
    def finish_request(self, request, client_address):
        '''Finish one request by instantiating RequestHandlerClass.'''
361
        self.RequestHandlerClass(request, client_address, self, self._log, self._read_sock)
362 363

    def _remove_unused_sock_file(self, sock_file):
364 365
        '''Try to remove the socket file. If the file is being used
        by one running xfrout process, exit from python.
366 367 368
        If it's not a socket file or nobody is listening
        , it will be removed. If it can't be removed, exit from python. '''
        if self._sock_file_in_use(sock_file):
369 370
            self._log.log_message("error", "Fail to start xfrout process, unix socket file '%s'"
                                 " is being used by another xfrout process\n" % sock_file)
371 372 373 374 375 376 377 378
            sys.exit(0)
        else:
            if not os.path.exists(sock_file):
                return

            try:
                os.unlink(sock_file)
            except OSError as err:
379
                self._log.log_message("error", '[b10-xfrout] Fail to remove file %s: %s\n' % (sock_file, err))
380
                sys.exit(0)
381

382
    def _sock_file_in_use(self, sock_file):
383 384
        '''Check whether the socket file 'sock_file' exists and
        is being used by one running xfrout process. If it is,
385 386 387 388 389 390 391
        return True, or else return False. '''
        try:
            sock = socket.socket(socket.AF_UNIX)
            sock.connect(sock_file)
        except socket.error as err:
            return False
        else:
392
            return True
393

394
    def shutdown(self):
395
        self._write_sock.send(b"shutdown") #terminate the xfrout session thread
396
        super().shutdown() # call the shutdown() of class socketserver_mixin.NoPollMixIn
397 398
        try:
            os.unlink(self._sock_file)
Jerry's avatar
Jerry committed
399 400
        except Exception as e:
            self._log.log_message("error", str(e))
401 402 403

    def update_config_data(self, new_config):
        '''Apply the new config setting of xfrout module. '''
Jerry's avatar
Jerry committed
404
        self._log.log_message('info', 'update config data start.')
405 406
        self._lock.acquire()
        self._max_transfers_out = new_config.get('transfers_out')
Jerry's avatar
Jerry committed
407
        self._log.log_message('info', 'max transfer out : %d', self._max_transfers_out)
408
        self._lock.release()
Jerry's avatar
Jerry committed
409
        self._log.log_message('info', 'update config data complete.')
410 411

    def get_db_file(self):
412 413 414 415 416 417
        file, is_default = self._cc.get_remote_config_value("Auth", "database_file")
        # 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)
        if is_default and "B10_FROM_BUILD" in os.environ:
            file = os.environ["B10_FROM_BUILD"] + os.sep + "bind10_zones.sqlite3"
418 419
        return file

420

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
    def increase_transfers_counter(self):
        '''Return False, if counter + 1 > max_transfers_out, or else
        return True
        '''
        ret = False
        self._lock.acquire()
        if self._transfers_counter < self._max_transfers_out:
            self._transfers_counter += 1
            ret = True
        self._lock.release()
        return ret

    def decrease_transfers_counter(self):
        self._lock.acquire()
        self._transfers_counter -= 1
        self._lock.release()

class XfroutServer:
    def __init__(self):
440
        self._unix_socket_server = None
441
        self._log = None
442
        self._listen_sock_file = UNIX_SOCKET_FILE
443 444 445 446
        self._shutdown_event = threading.Event()
        self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
        self._config_data = self._cc.get_full_config()
        self._cc.start()
447
        self._cc.add_remote_config(AUTH_SPECFILE_LOCATION);
Jerry's avatar
Jerry committed
448
        self._log = isc.log.NSLogger(self._config_data.get('log_name'), self._config_data.get('log_file'),
Jerry's avatar
Jerry committed
449 450
                                self._config_data.get('log_severity'), self._config_data.get('log_versions'),
                                self._config_data.get('log_max_bytes'), True)
451
        self._start_xfr_query_listener()
452
        self._start_notifier()
453

454 455
    def _start_xfr_query_listener(self):
        '''Start a new thread to accept xfr query. '''
456
        self._unix_socket_server = UnixSockServer(self._listen_sock_file, XfroutSession,
457
                                                  self._shutdown_event, self._config_data,
458
                                                  self._cc, self._log);
459
        listener = threading.Thread(target=self._unix_socket_server.serve_forever)
460
        listener.start()
461

462 463 464
    def _start_notifier(self):
        datasrc = self._unix_socket_server.get_db_file()
        self._notifier = notify_out.NotifyOut(datasrc, self._log)
Michal Vaner's avatar
Michal Vaner committed
465
        self._notifier.dispatcher()
466

467 468
    def send_notify(self, zone_name, zone_class):
        self._notifier.send_notify(zone_name, zone_class)
469 470 471 472 473 474 475 476 477

    def config_handler(self, new_config):
        '''Update config data. TODO. Do error check'''
        answer = create_answer(0)
        for key in new_config:
            if key not in self._config_data:
                answer = create_answer(1, "Unknown config data: " + str(key))
                continue
            self._config_data[key] = new_config[key]
Michal Vaner's avatar
Michal Vaner committed
478

479
        if self._log:
Jerry's avatar
Jerry committed
480
            self._log.update_config(new_config)
481

482 483
        if self._unix_socket_server:
            self._unix_socket_server.update_config_data(self._config_data)
484

485 486 487 488
        return answer


    def shutdown(self):
489
        ''' shutdown the xfrout process. The thread which is doing zone transfer-out should be
490 491
        terminated.
        '''
492 493 494

        global xfrout_server
        xfrout_server = None #Avoid shutdown is called twice
495
        self._shutdown_event.set()
496
        self._notifier.shutdown()
497 498
        if self._unix_socket_server:
            self._unix_socket_server.shutdown()
499

500
        # Wait for all threads to terminate
501 502 503 504 505 506 507 508
        main_thread = threading.currentThread()
        for th in threading.enumerate():
            if th is main_thread:
                continue
            th.join()

    def command_handler(self, cmd, args):
        if cmd == "shutdown":
509
            self._log.log_message("info", "Received shutdown command.")
510 511
            self.shutdown()
            answer = create_answer(0)
Michal Vaner's avatar
Michal Vaner committed
512

513
        elif cmd == notify_out.ZONE_NEW_DATA_READY_CMD:
514
            zone_name = args.get('zone_name')
515 516
            zone_class = args.get('zone_class')
            if zone_name and zone_class:
517 518
                self._log.log_message("info", "zone '%s/%s': receive notify others command" \
                                       % (zone_name, zone_class))
519
                self.send_notify(zone_name, zone_class)
520 521 522 523
                answer = create_answer(0)
            else:
                answer = create_answer(1, "Bad command parameter:" + str(args))

524
        else:
525 526
            answer = create_answer(1, "Unknown command:" + str(cmd))

Michal Vaner's avatar
Michal Vaner committed
527
        return answer
528 529 530 531

    def run(self):
        '''Get and process all commands sent from cfgmgr or other modules. '''
        while not self._shutdown_event.is_set():
532
            self._cc.check_command(False)
533 534 535 536 537


xfrout_server = None

def signal_handler(signal, frame):
538
    if xfrout_server:
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
        xfrout_server.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:
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
Jerry's avatar
Jerry committed
555
        VERBOSE_MODE = options.verbose
556 557 558 559 560

        set_signal_handler()
        xfrout_server = XfroutServer()
        xfrout_server.run()
    except KeyboardInterrupt:
Jeremy C. Reed's avatar
Jeremy C. Reed committed
561
        sys.stderr.write("[b10-xfrout] exit xfrout process\n")
562
    except SessionError as e:
563
        sys.stderr.write("[b10-xfrout] Error creating xfrout, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
564
                           "is the command channel daemon running?\n")
565
    except SessionTimeout as e:
566
        sys.stderr.write("[b10-xfrout] Error creating xfrout, "
Jeremy C. Reed's avatar
Jeremy C. Reed committed
567
                           "is the configuration manager running?\n")
568
    except ModuleCCSessionError as e:
Jeremy C. Reed's avatar
Jeremy C. Reed committed
569
        sys.stderr.write("[b10-xfrout] exit xfrout process:%s\n" % str(e))
570

571 572 573
    if xfrout_server:
        xfrout_server.shutdown()