#!@PYTHON@ # Copyright (C) 2011 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. import sys; sys.path.append ('@@PYTHONPATH@@') import isc import bind10_config from isc.dns import * from isc.config.ccsession import * from isc.cc import SessionError, SessionTimeout import isc.util.process import isc.util.io.socketsession import select import errno from isc.log_messages.ddns_messages import * from optparse import OptionParser, OptionValueError import os import os.path import signal import socket isc.log.init("b10-ddns") logger = isc.log.Logger("ddns") TRACE_BASIC = logger.DBGLVL_TRACE_BASIC DATA_PATH = bind10_config.DATA_PATH SOCKET_FILE = DATA_PATH + '/ddns_socket' if "B10_FROM_SOURCE" in os.environ: DATA_PATH = os.environ['B10_FROM_SOURCE'] + "/src/bin/ddns" SPECFILE_LOCATION = DATA_PATH + "/ddns.spec" isc.util.process.rename() class DDNSConfigError(Exception): '''An exception indicating an error in updating ddns configuration. This exception is raised when the ddns process encounters an error in handling configuration updates. Not all syntax error can be caught at the module-CC layer, so ddns needs to (explicitly or implicitly) validate the given configuration data itself. When it finds an error it raises this exception (either directly or by converting an exception from other modules) as a unified error in configuration. ''' pass class DDNSSessionError(Exception): '''An exception raised for some unexpected events during a ddns session. ''' pass class DDNSSession: '''Class to handle one DDNS update''' def __init__(self): '''Initialize a DDNS Session''' pass def clear_socket(): ''' Removes the socket file, if it exists. ''' if os.path.exists(SOCKET_FILE): os.remove(SOCKET_FILE) class DDNSServer: def __init__(self, cc_session=None): ''' Initialize the DDNS Server. This sets up a ModuleCCSession for the BIND 10 system. Parameters: cc_session: If None (default), a new ModuleCCSession will be set up. If specified, the given session will be used. This is mainly used for testing. ''' if cc_session is not None: self._cc = cc_session else: self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler) self._config_data = self._cc.get_full_config() self._cc.start() self._shutdown = False # List of the session receivers where we get the requests self._socksession_receivers = {} clear_socket() self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._listen_socket.bind(SOCKET_FILE) self._listen_socket.listen(16) def config_handler(self, new_config): '''Update config data.''' answer = create_answer(0) return answer def command_handler(self, cmd, args): ''' Handle a CC session command, as sent from bindctl or other BIND 10 modules. ''' if cmd == "shutdown": logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND) self.trigger_shutdown() answer = create_answer(0) else: answer = create_answer(1, "Unknown command: " + str(cmd)) return answer def trigger_shutdown(self): '''Initiate a shutdown sequence. This method is expected to be called in various ways including in the middle of a signal handler, and is designed to be as simple as possible to minimize side effects. Actual shutdown will take place in a normal control flow. ''' logger.info(DDNS_SHUTDOWN) self._shutdown = True def shutdown_cleanup(self): ''' Perform any cleanup that is necessary when shutting down the server. Do NOT call this to initialize shutdown, use trigger_shutdown(). Currently, it does nothing, but cleanup routines are expected. ''' pass def accept(self): """ Accept another connection and create the session receiver. """ socket = self._listen_socket.accept() fileno = socket.fileno() logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno, socket.getpeername()) receiver = isc.util.io.socketsession.SocketSessionReceiver(socket) self._socksession_receivers[fileno] = (socket, receiver) def handle_request(self, request): """ This is the place where the actual DDNS processing is done. Other methods are either subroutines of this method or methods doing the uninteresting "accounting" stuff, like accepting socket, initialization, etc. It is called with the request being session as received from SocketSessionReceiver, i.e. tuple (socket, local_address, remote_address, data). """ # TODO: Implement the magic pass def handle_session(self, fileno): """ Handle incoming session on the socket with given fileno. """ logger.debug(TRACE_BASIC, DDNS_SESSION, fileno) (socket, receiver) = self._socksession_receivers[fileno] try: self.handle_request(receiver.pop()) except isc.util.io.socketsession.SocketSessionError as se: # No matter why this failed, the connection is in unknown, possibly # broken state. So, we close the socket and remove the receiver. del self._socksession_receivers[fileno] socket.close() logger.warn(DDNS_DROP_CONN, fileno, se) def run(self): ''' Get and process all commands sent from cfgmgr or other modules. This loops waiting for events until self.shutdown() has been called. ''' logger.info(DDNS_RUNNING) cc_fileno = self._cc.get_socket().fileno() listen_fileno = self._listen_socket.fileno() while not self._shutdown: # In this event loop we propage most of exceptions, which will # subsequently kill the b10-ddns process, but ideally it would be # better to catch any exceptions that b10-ddns can recover from. # We currently have no exception hierarchy to make such a # distinction easily, but once we do, we should catch and handle # non fatal exceptions here and continue the process. try: (reads, writes, exceptions) = \ select.select([cc_fileno, listen_fileno] + list(self._socksession_receivers.keys()), [], []) except select.error as se: # In case it is just interrupted, we continue like nothing # happened if se.args[0] == errno.EINTR: (reads, writes, exceptions) = ([], [], []) else: raise for fileno in reads: if fileno == cc_fileno: self._cc.check_command(True) elif fileno == listen_fileno: self.accept() else: self.handle_session(fileno) self.shutdown_cleanup() logger.info(DDNS_STOPPED) def create_signal_handler(ddns_server): ''' This creates a signal_handler for use in set_signal_handler, which shuts down the given DDNSServer (or any object that has a shutdown() method) ''' def signal_handler(signal, frame): ''' Handler for process signals. Since only signals to shut down are sent here, the actual signal is not checked and the server is simply shut down. ''' ddns_server.trigger_shutdown() return signal_handler def set_signal_handler(signal_handler): ''' Sets the signal handler(s). ''' signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) def set_cmd_options(parser): ''' Helper function to set command-line options ''' parser.add_option("-v", "--verbose", dest="verbose", action="store_true", help="display more about what is going on") def main(ddns_server=None): ''' The main function. Parameters: ddns_server: If None (default), a DDNSServer object is initialized. If specified, the given DDNSServer will be used. This is mainly used for testing. cc_session: If None (default), a new ModuleCCSession will be set up. If specified, the given session will be used. This is mainly used for testing. ''' try: parser = OptionParser() set_cmd_options(parser) (options, args) = parser.parse_args() if options.verbose: print("[b10-ddns] Warning: -v verbose option is ignored at this point.") if ddns_server is None: ddns_server = DDNSServer() set_signal_handler(create_signal_handler(ddns_server)) ddns_server.run() except KeyboardInterrupt: logger.info(DDNS_STOPPED_BY_KEYBOARD) except SessionError as e: logger.error(DDNS_CC_SESSION_ERROR, str(e)) except ModuleCCSessionError as e: logger.error(DDNS_MODULECC_SESSION_ERROR, str(e)) except DDNSConfigError as e: logger.error(DDNS_CONFIG_ERROR, str(e)) except SessionTimeout as e: logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR) except Exception as e: logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e)) clear_socket() if '__main__' == __name__: main()