ddns.py.in 10.3 KB
Newer Older
1
2
#!@PYTHON@

3
# Copyright (C) 2011  Internet Systems Consortium.
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# 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
21
import bind10_config
22
23
24
25
from isc.dns import *
from isc.config.ccsession import *
from isc.cc import SessionError, SessionTimeout
import isc.util.process
26
import isc.util.io.socketsession
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
27
import select
28
import errno
29
30
31

from isc.log_messages.ddns_messages import *

32
33
from optparse import OptionParser, OptionValueError
import os
34
import os.path
35
import signal
36
import socket
37

38
39
isc.log.init("b10-ddns")
logger = isc.log.Logger("ddns")
40
TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
41
42

DATA_PATH = bind10_config.DATA_PATH
43
SOCKET_FILE = DATA_PATH + '/ddns_socket'
44
45
if "B10_FROM_SOURCE" in os.environ:
    DATA_PATH = os.environ['B10_FROM_SOURCE'] + "/src/bin/ddns"
46
47
SPECFILE_LOCATION = DATA_PATH + "/ddns.spec"

48
49
50
51

isc.util.process.rename()

class DDNSConfigError(Exception):
52
    '''An exception indicating an error in updating ddns configuration.
53

54
    This exception is raised when the ddns process encounters an error in
55
56
57
58
59
    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.
60
    '''
61
62
63
    pass

class DDNSSessionError(Exception):
64
    '''An exception raised for some unexpected events during a ddns session.
65
66
67
    '''
    pass

68
class DDNSSession:
69
    '''Class to handle one DDNS update'''
70

71
    def __init__(self):
72
        '''Initialize a DDNS Session'''
73
74
        pass

75
76
77
78
79
80
81
def clear_socket():
    '''
    Removes the socket file, if it exists.
    '''
    if os.path.exists(SOCKET_FILE):
        os.remove(SOCKET_FILE)

82
class DDNSServer:
83
    def __init__(self, cc_session=None):
84
85
86
        '''
        Initialize the DDNS Server.
        This sets up a ModuleCCSession for the BIND 10 system.
Jelte Jansen's avatar
Jelte Jansen committed
87
        Parameters:
88
        cc_session: If None (default), a new ModuleCCSession will be set up.
Jelte Jansen's avatar
Jelte Jansen committed
89
90
                    If specified, the given session will be used. This is
                    mainly used for testing.
91
        '''
Jelte Jansen's avatar
Jelte Jansen committed
92
93
94
95
96
97
98
        if cc_session is not None:
            self._cc = cc_session
        else:
            self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
                                                  self.config_handler,
                                                  self.command_handler)

99
100
        self._config_data = self._cc.get_full_config()
        self._cc.start()
101
        self._shutdown = False
102
103
        # List of the session receivers where we get the requests
        self._socksession_receivers = {}
104
105
106
107
        clear_socket()
        self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self._listen_socket.bind(SOCKET_FILE)
        self._listen_socket.listen(16)
108
109
110
111
112
113
114

    def config_handler(self, new_config):
        '''Update config data.'''
        answer = create_answer(0)
        return answer

    def command_handler(self, cmd, args):
115
116
117
118
        '''
        Handle a CC session command, as sent from bindctl or other
        BIND 10 modules.
        '''
119
120
        if cmd == "shutdown":
            logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
Jelte Jansen's avatar
Jelte Jansen committed
121
            self.trigger_shutdown()
122
123
            answer = create_answer(0)
        else:
Jelte Jansen's avatar
Jelte Jansen committed
124
            answer = create_answer(1, "Unknown command: " + str(cmd))
125
126
        return answer

Jelte Jansen's avatar
Jelte Jansen committed
127
128
129
130
131
132
133
134
    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.

135
        '''
136
        logger.info(DDNS_SHUTDOWN)
Jelte Jansen's avatar
Jelte Jansen committed
137
138
139
140
141
142
143
144
145
146
        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
147

148
149
150
151
    def accept(self):
        """
        Accept another connection and create the session receiver.
        """
152
153
        socket = self._listen_socket.accept()
        fileno = socket.fileno()
154
155
156
        logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno, socket.getpeername())
        receiver = isc.util.io.socketsession.SocketSessionReceiver(socket)
        self._socksession_receivers[fileno] = (socket, receiver)
157

158
159
    def handle_request(self, request):
        """
160
161
162
163
        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.
164
165

        It is called with the request being session as received from
166
        SocketSessionReceiver, i.e. tuple
167
168
169
170
171
        (socket, local_address, remote_address, data).
        """
        # TODO: Implement the magic
        pass

172
    def handle_session(self, fileno):
173
174
175
        """
        Handle incoming event on the socket with given fileno.
        """
176
177
        logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
        (socket, receiver) = self._socksession_receivers[fileno]
178
        try:
179
            self.handle_request(receiver.pop())
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
180
        except isc.util.io.socketsession.SocketSessionError as se:
181
            # No matter why this failed, the connection is in unknown, possibly
182
183
            # broken state. So, we close the socket and remove the receiver.
            del self._socksession_receivers[fileno]
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
184
185
            socket.close()
            logger.warn(DDNS_DROP_CONN, fileno, se)
186

187
    def run(self):
188
189
190
191
        '''
        Get and process all commands sent from cfgmgr or other modules.
        This loops waiting for events until self.shutdown() has been called.
        '''
192
        logger.info(DDNS_RUNNING)
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
193
194
        cc_fileno = self._cc.get_socket().fileno()
        listen_fileno = self._listen_socket.fileno()
195
        while not self._shutdown:
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
196
197
198
199
200
201
            # 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.
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
202

203
204
205
206
207
208
209
210
211
212
213
214
            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
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
215
216
            for fileno in reads:
                if fileno == cc_fileno:
217
                    self._cc.check_command(True)
Michal 'vorner' Vaner's avatar
Michal 'vorner' Vaner committed
218
219
220
                elif fileno == listen_fileno:
                    self.accept()
                else:
221
                    self.handle_session(fileno)
Jelte Jansen's avatar
Jelte Jansen committed
222
        self.shutdown_cleanup()
223
        logger.info(DDNS_STOPPED)
224

Jelte Jansen's avatar
Jelte Jansen committed
225
def create_signal_handler(ddns_server):
226
    '''
Jelte Jansen's avatar
Jelte Jansen committed
227
228
229
    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)
230
    '''
Jelte Jansen's avatar
Jelte Jansen committed
231
232
233
234
235
236
    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.
        '''
Jelte Jansen's avatar
Jelte Jansen committed
237
        ddns_server.trigger_shutdown()
Jelte Jansen's avatar
Jelte Jansen committed
238
    return signal_handler
239

Jelte Jansen's avatar
Jelte Jansen committed
240
def set_signal_handler(signal_handler):
241
242
243
    '''
    Sets the signal handler(s).
    '''
244
245
246
247
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)

def set_cmd_options(parser):
248
249
250
    '''
    Helper function to set command-line options
    '''
251
252
253
    parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
            help="display more about what is going on")

254
255
256
257
258
259
260
261
262
263
264
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.
    '''
265
266
267
268
    try:
        parser = OptionParser()
        set_cmd_options(parser)
        (options, args) = parser.parse_args()
Jelte Jansen's avatar
Jelte Jansen committed
269
270
        if options.verbose:
            print("[b10-ddns] Warning: -v verbose option is ignored at this point.")
271

272
273
        if ddns_server is None:
            ddns_server = DDNSServer()
Jelte Jansen's avatar
Jelte Jansen committed
274
        set_signal_handler(create_signal_handler(ddns_server))
275
276
        ddns_server.run()
    except KeyboardInterrupt:
Jelte Jansen's avatar
Jelte Jansen committed
277
        logger.info(DDNS_STOPPED_BY_KEYBOARD)
278
279
280
281
282
283
284
285
    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)
286
287
    except Exception as e:
        logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
288
    clear_socket()
289
290
291

if '__main__' == __name__:
    main()