stats_httpd.py.in 30.4 KB
Newer Older
Naoki Kambe's avatar
Naoki Kambe committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!@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.

Naoki Kambe's avatar
Naoki Kambe committed
18
19
20
21
"""
A standalone HTTP server for HTTP/XML interface of statistics in BIND 10

"""
Naoki Kambe's avatar
Naoki Kambe committed
22
23
24
25
26
27
28
29
30
31
import sys; sys.path.append ('@@PYTHONPATH@@')
import os
import time
import errno
import select
from optparse import OptionParser, OptionValueError
import http.server
import socket
import string
import xml.etree.ElementTree
32
import urllib.parse
Naoki Kambe's avatar
Naoki Kambe committed
33
34
35
36
37

import isc.cc
import isc.config
import isc.util.process

38
import isc.log
39
from isc.log_messages.stats_httpd_messages import *
40
41
42
43

isc.log.init("b10-stats-httpd")
logger = isc.log.Logger("stats-httpd")

44
45
46
# Some constants for debug levels.
DBG_STATHTTPD_INIT = logger.DBGLVL_START_SHUT
DBG_STATHTTPD_MESSAGING = logger.DBGLVL_COMMAND
47

Naoki Kambe's avatar
Naoki Kambe committed
48
49
50
51
# If B10_FROM_SOURCE 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_SOURCE" in os.environ:
52
53
    BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
        "src" + os.sep + "bin" + os.sep + "stats"
Naoki Kambe's avatar
Naoki Kambe committed
54
55
56
57
58
59
60
61
62
63
64
else:
    PREFIX = "@prefix@"
    DATAROOTDIR = "@datarootdir@"
    BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
    BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd.spec"
XML_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xml.tpl"
XSD_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsd.tpl"
XSL_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsl.tpl"

# These variables are paths part of URL.
65
66
67
68
# eg. "http://${address}" + XXX_URL_PATH
XML_URL_PATH = '/bind10/statistics/xml'
XSD_URL_PATH = '/bind10/statistics/xsd'
XSL_URL_PATH = '/bind10/statistics/xsl'
Naoki Kambe's avatar
Naoki Kambe committed
69
# TODO: This should be considered later.
70
XSD_NAMESPACE = 'http://bind10.isc.org/bind10'
Naoki Kambe's avatar
Naoki Kambe committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

# Assign this process name
isc.util.process.rename()

class HttpHandler(http.server.BaseHTTPRequestHandler):
    """HTTP handler class for HttpServer class. The class inhrits the super
    class http.server.BaseHTTPRequestHandler. It implemets do_GET()
    and do_HEAD() and orverrides log_message()"""
    def do_GET(self):
        body = self.send_head()
        if body is not None:
            self.wfile.write(body.encode())

    def do_HEAD(self):
        self.send_head()

    def send_head(self):
        try:
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
            req_path = self.path
            req_path = urllib.parse.urlsplit(req_path).path
            req_path = urllib.parse.unquote(req_path)
            req_path = os.path.normpath(req_path)
            path_dirs = req_path.split('/')
            path_dirs = [ d for d in filter(None, path_dirs) ]
            module_name = None
            item_name = None
            # in case of /bind10/statistics/xxx/YYY/zzz/
            if len(path_dirs) >= 5:
                item_name = path_dirs[4]
            # in case of /bind10/statistics/xxx/YYY/
            if len(path_dirs) >= 4:
                module_name = path_dirs[3]
            if req_path.startswith(XML_URL_PATH):
                body = self.server.xml_handler(module_name, item_name)
            elif req_path.startswith(XSD_URL_PATH):
                body = self.server.xsd_handler(module_name, item_name)
            elif req_path.startswith(XSL_URL_PATH):
                body = self.server.xsl_handler(module_name, item_name)
Naoki Kambe's avatar
Naoki Kambe committed
109
            else:
110
                if req_path == '/' and 'Host' in self.headers.keys():
Naoki Kambe's avatar
Naoki Kambe committed
111
                    # redirect to XML URL only when requested with '/'
112
113
114
                    self.send_response(302)
                    self.send_header(
                        "Location",
115
                        "http://" + self.headers.get('Host') + XML_URL_PATH)
116
117
118
119
120
121
                    self.end_headers()
                    return None
                else:
                    # Couldn't find HOST
                    self.send_error(404)
                    return None
Naoki Kambe's avatar
Naoki Kambe committed
122
123
        except StatsHttpdError as err:
            self.send_error(500)
124
            logger.error(STATHTTPD_SERVER_ERROR, err)
Naoki Kambe's avatar
Naoki Kambe committed
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
            return None
        else:
            self.send_response(200)
            self.send_header("Content-type", "text/xml")
            self.send_header("Content-Length", len(body))
            self.end_headers()
            return body

class HttpServerError(Exception):
    """Exception class for HttpServer class. It is intended to be
    passed from the HttpServer object to the StatsHttpd object."""
    pass

class HttpServer(http.server.HTTPServer):
    """HTTP Server class. The class inherits the super
    http.server.HTTPServer. Some parameters are specified as
    arguments, which are xml_handler, xsd_handler, xsl_handler, and
    log_writer. These all are parameters which the StatsHttpd object
    has. The handler parameters are references of functions which
    return body of each document. The last parameter log_writer is
    reference of writer function to just write to
    sys.stderr.write. They are intended to be referred by HttpHandler
    object."""
    def __init__(self, server_address, handler,
149
                 xml_handler, xsd_handler, xsl_handler, log_writer):
Naoki Kambe's avatar
Naoki Kambe committed
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
        self.server_address = server_address
        self.xml_handler = xml_handler
        self.xsd_handler = xsd_handler
        self.xsl_handler = xsl_handler
        self.log_writer = log_writer
        http.server.HTTPServer.__init__(self, server_address, handler)

class StatsHttpdError(Exception):
    """Exception class for StatsHttpd class. It is intended to be
    thrown from the the StatsHttpd object to the HttpHandler object or
    main routine."""
    pass

class StatsHttpd:
    """The main class of HTTP server of HTTP/XML interface for
    statistics module. It handles HTTP requests, and command channel
    and config channel CC session. It uses select.select function
    while waiting for clients requests."""
168
    def __init__(self):
Naoki Kambe's avatar
Naoki Kambe committed
169
170
171
172
173
174
        self.running = False
        self.poll_intval = 0.5
        self.write_log = sys.stderr.write
        self.mccs = None
        self.httpd = []
        self.open_mccs()
Naoki Kambe's avatar
Naoki Kambe committed
175
        self.config = {}
Naoki Kambe's avatar
Naoki Kambe committed
176
        self.load_config()
Naoki Kambe's avatar
Naoki Kambe committed
177
178
179
        self.http_addrs = []
        self.mccs.start()
        self.open_httpd()
Naoki Kambe's avatar
Naoki Kambe committed
180
181
182
183

    def open_mccs(self):
        """Opens a ModuleCCSession object"""
        # create ModuleCCSession
184
        logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_STARTING_CC_SESSION)
Naoki Kambe's avatar
Naoki Kambe committed
185
186
187
188
189
190
191
192
        self.mccs = isc.config.ModuleCCSession(
            SPECFILE_LOCATION, self.config_handler, self.command_handler)
        self.cc_session = self.mccs._session

    def close_mccs(self):
        """Closes a ModuleCCSession object"""
        if self.mccs is None:
            return
193

194
        logger.debug(DBG_STATHTTPD_INIT, STATHTTPD_CLOSING_CC_SESSION)
Naoki Kambe's avatar
Naoki Kambe committed
195
196
197
198
199
200
201
        self.mccs.close()
        self.mccs = None

    def load_config(self, new_config={}):
        """Loads configuration from spec file or new configuration
        from the config manager"""
        # load config
Naoki Kambe's avatar
Naoki Kambe committed
202
203
204
205
206
207
        if len(self.config) == 0:
            self.config = dict([
                (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
                for itm in self.mccs.get_module_spec().get_config_spec()
                ])
        self.config.update(new_config)
208
        # set addresses and ports for HTTP
Naoki Kambe's avatar
Naoki Kambe committed
209
210
211
212
213
214
        addrs = []
        if 'listen_on' in self.config:
            for cf in self.config['listen_on']:
                if 'address' in cf and 'port' in cf:
                    addrs.append((cf['address'], cf['port']))
        self.http_addrs = addrs
Naoki Kambe's avatar
Naoki Kambe committed
215
216
217
218
219
220
221

    def open_httpd(self):
        """Opens sockets for HTTP. Iterating each HTTP address to be
        configured in spec file"""
        for addr in self.http_addrs:
            self.httpd.append(self._open_httpd(addr))

222
223
    def _open_httpd(self, server_address):
        httpd = None
Naoki Kambe's avatar
Naoki Kambe committed
224
        try:
225
            # get address family for the server_address before
226
227
228
229
230
231
            # creating HttpServer object. If a specified address is
            # not numerical, gaierror may be thrown.
            address_family = socket.getaddrinfo(
                server_address[0], server_address[1], 0,
                socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST
                )[0][0]
232
            HttpServer.address_family = address_family
Naoki Kambe's avatar
Naoki Kambe committed
233
234
235
            httpd = HttpServer(
                server_address, HttpHandler,
                self.xml_handler, self.xsd_handler, self.xsl_handler,
236
                self.write_log)
237
            logger.info(STATHTTPD_STARTED, server_address[0],
238
                        server_address[1])
239
240
241
242
243
244
245
246
247
            return httpd
        except (socket.gaierror, socket.error,
                OverflowError, TypeError) as err:
           if httpd:
                httpd.server_close()
           raise HttpServerError(
               "Invalid address %s, port %s: %s: %s" %
               (server_address[0], server_address[1],
                err.__class__.__name__, err))
Naoki Kambe's avatar
Naoki Kambe committed
248
249
250

    def close_httpd(self):
        """Closes sockets for HTTP"""
251
252
        while len(self.httpd)>0:
            ht = self.httpd.pop()
253
            logger.info(STATHTTPD_CLOSING, ht.server_address[0],
254
                        ht.server_address[1])
Naoki Kambe's avatar
Naoki Kambe committed
255
256
257
258
259
260
261
262
263
264
265
            ht.server_close()

    def start(self):
        """Starts StatsHttpd objects to run. Waiting for client
        requests by using select.select functions"""
        self.running = True
        while self.running:
            try:
                (rfd, wfd, xfd) = select.select(
                    self.get_sockets(), [], [], self.poll_intval)
            except select.error as err:
266
267
                # select.error exception is caught only in the case of
                # EINTR, or in other cases it is just thrown.
Naoki Kambe's avatar
Naoki Kambe committed
268
269
270
                if err.args[0] == errno.EINTR:
                    (rfd, wfd, xfd) = ([], [], [])
                else:
271
                    raise
Naoki Kambe's avatar
Naoki Kambe committed
272
            # FIXME: This module can handle only one request at a
273
274
275
            # time. If someone sends only part of the request, we block
            # waiting for it until we time out.
            # But it isn't so big issue for administration purposes.
Naoki Kambe's avatar
Naoki Kambe committed
276
277
278
279
280
281
282
283
284
285
286
287
288
            for fd in rfd + xfd:
                if fd == self.mccs.get_socket():
                    self.mccs.check_command(nonblock=False)
                    continue
                for ht in self.httpd:
                    if fd == ht.socket:
                        ht.handle_request()
                        break
        self.stop()

    def stop(self):
        """Stops the running StatsHttpd objects. Closes CC session and
        HTTP handling sockets"""
289
        logger.info(STATHTTPD_SHUTDOWN)
Naoki Kambe's avatar
Naoki Kambe committed
290
291
        self.close_httpd()
        self.close_mccs()
Naoki Kambe's avatar
Naoki Kambe committed
292
        self.running = False
Naoki Kambe's avatar
Naoki Kambe committed
293
294
295
296
297
298
299
300
301
302
303
304
305
306

    def get_sockets(self):
        """Returns sockets to select.select"""
        sockets = []
        if self.mccs is not None:
            sockets.append(self.mccs.get_socket())
        if len(self.httpd) > 0:
            for ht in self.httpd:
                sockets.append(ht.socket)
        return sockets

    def config_handler(self, new_config):
        """Config handler for the ModuleCCSession object. It resets
        addresses and ports to listen HTTP requests on."""
307
        logger.debug(DBG_STATHTTPD_MESSAGING, STATHTTPD_HANDLE_CONFIG,
308
                   new_config)
Naoki Kambe's avatar
Naoki Kambe committed
309
310
311
        errors = []
        if not self.mccs.get_module_spec().\
                validate_config(False, new_config, errors):
Naoki Kambe's avatar
Naoki Kambe committed
312
                return isc.config.ccsession.create_answer(
Naoki Kambe's avatar
Naoki Kambe committed
313
                    1, ", ".join(errors))
Naoki Kambe's avatar
Naoki Kambe committed
314
315
316
        # backup old config
        old_config = self.config.copy()
        self.load_config(new_config)
Naoki Kambe's avatar
Naoki Kambe committed
317
318
319
320
321
        # If the http sockets aren't opened or
        # if new_config doesn't have'listen_on', it returns
        if len(self.httpd) == 0 or 'listen_on' not in new_config:
            return isc.config.ccsession.create_answer(0)
        self.close_httpd()
Naoki Kambe's avatar
Naoki Kambe committed
322
323
324
        try:
            self.open_httpd()
        except HttpServerError as err:
325
            logger.error(STATHTTPD_SERVER_ERROR, err)
Naoki Kambe's avatar
Naoki Kambe committed
326
            # restore old config
327
328
            self.load_config(old_config)
            self.open_httpd()
329
            return isc.config.ccsession.create_answer(1, str(err))
Naoki Kambe's avatar
Naoki Kambe committed
330
331
332
333
334
335
336
        else:
            return isc.config.ccsession.create_answer(0)

    def command_handler(self, command, args):
        """Command handler for the ModuleCCSesson object. It handles
        "status" and "shutdown" commands."""
        if command == "status":
337
338
            logger.debug(DBG_STATHTTPD_MESSAGING,
                         STATHTTPD_RECEIVED_STATUS_COMMAND)
Naoki Kambe's avatar
Naoki Kambe committed
339
340
341
            return isc.config.ccsession.create_answer(
                0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
        elif command == "shutdown":
342
343
            logger.debug(DBG_STATHTTPD_MESSAGING,
                         STATHTTPD_RECEIVED_SHUTDOWN_COMMAND)
Naoki Kambe's avatar
Naoki Kambe committed
344
            self.running = False
345
            return isc.config.ccsession.create_answer(0)
Naoki Kambe's avatar
Naoki Kambe committed
346
        else:
347
348
            logger.debug(DBG_STATHTTPD_MESSAGING,
                         STATHTTPD_RECEIVED_UNKNOWN_COMMAND, command)
Naoki Kambe's avatar
Naoki Kambe committed
349
350
351
            return isc.config.ccsession.create_answer(
                1, "Unknown command: " + str(command))

352
    def get_stats_data(self, owner=None, name=None):
Naoki Kambe's avatar
Naoki Kambe committed
353
        """Requests statistics data to the Stats daemon and returns
354
355
356
357
358
359
360
361
        the data which obtains from it. args are owner and name."""
        param = {}
        if owner is None and name is None:
            param = None
        if owner is not None:
            param['owner'] = owner
        if name is not None:
            param['name'] = name
Naoki Kambe's avatar
Naoki Kambe committed
362
363
        try:
            seq = self.cc_session.group_sendmsg(
364
                isc.config.ccsession.create_command('show', param), 'Stats')
Naoki Kambe's avatar
Naoki Kambe committed
365
366
367
368
369
370
371
372
373
374
375
376
377
            (answer, env) = self.cc_session.group_recvmsg(False, seq)
            if answer:
                (rcode, value) = isc.config.ccsession.parse_answer(answer)
        except (isc.cc.session.SessionTimeout,
                isc.cc.session.SessionError) as err:
            raise StatsHttpdError("%s: %s" %
                                  (err.__class__.__name__, err))
        else:
            if rcode == 0:
                return value
            else:
                raise StatsHttpdError("Stats module: %s" % str(value))

378
    def get_stats_spec(self, owner=None, name=None):
379
        """Requests statistics data to the Stats daemon and returns
380
381
382
383
384
385
386
387
        the data which obtains from it. args are owner and name."""
        param = {}
        if owner is None and name is None:
            param = None
        if owner is not None:
            param['owner'] = owner
        if name is not None:
            param['name'] = name
388
389
        try:
            seq = self.cc_session.group_sendmsg(
390
                isc.config.ccsession.create_command('showschema', param), 'Stats')
391
392
393
394
395
396
397
398
399
400
401
402
            (answer, env) = self.cc_session.group_recvmsg(False, seq)
            if answer:
                (rcode, value) = isc.config.ccsession.parse_answer(answer)
                if rcode == 0:
                    return value
                else:
                    raise StatsHttpdError("Stats module: %s" % str(value))
        except (isc.cc.session.SessionTimeout,
                isc.cc.session.SessionError) as err:
            raise StatsHttpdError("%s: %s" %
                                  (err.__class__.__name__, err))

403
404

    def xml_handler(self, module_name=None, item_name=None):
405
406
        """Handler which requests to Stats daemon to obtain statistics
        data and returns the body of XML document"""
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422

        def stats_data2xml(stats_spec, stats_data, xml_elem):
            """Internal use for xml_handler. Reads stats_data and
            stats_spec specified as first and second arguments, and
            modify the xml object specified as third
            argument. xml_elem must be modified and always returns
            None."""
            # assumed started with module_spec or started with
            # item_spec in statistics
            if type(stats_spec) is dict:
                # assumed started with module_spec
                if 'item_name' not in stats_spec \
                        and 'item_type' not in stats_spec:
                    for module_name in stats_spec.keys():
                        elem = xml.etree.ElementTree.Element(module_name)
                        stats_data2xml(stats_spec[module_name],
423
                                       stats_data[module_name], elem)
424
425
426
                        xml_elem.append(elem)
                # started with item_spec in statistics
                else:
427
                    elem = xml.etree.ElementTree.Element(stats_spec['item_name'])
428
429
                    if stats_spec['item_type'] == 'map':
                        stats_data2xml(stats_spec['map_item_spec'],
430
431
                                       stats_data,
                                       elem)
432
433
434
                    elif stats_spec['item_type'] == 'list':
                        for item in stats_data:
                            stats_data2xml(stats_spec['list_item_spec'],
435
                                           item, elem)
436
437
                    else:
                        elem.text = str(stats_data)
438
                    xml_elem.append(elem)
439
440
441
442
            # assumed started with stats_spec
            elif type(stats_spec) is list:
                for item_spec in stats_spec:
                    stats_data2xml(item_spec,
443
444
                                   stats_data[item_spec['item_name']],
                                   xml_elem)
445
446
447
448
            else:
                xml_elem.text = str(stats_data)
            return None

449
450
451
452
453
454
455
        stats_spec = self.get_stats_spec(module_name, item_name)
        stats_data = self.get_stats_data(module_name, item_name)
        xml_elem = xml.etree.ElementTree.Element(
            'bind10:statistics',
            attrib={ 'xsi:schemaLocation' : XSD_NAMESPACE + ' ' + XSD_URL_PATH,
                     'xmlns:bind10' : XSD_NAMESPACE,
                     'xmlns:xsi' : "http://www.w3.org/2001/XMLSchema-instance" })
456
        stats_data2xml(stats_spec, stats_data, xml_elem)
457
458
459
460
461
462
463
464
        # The coding conversion is tricky. xml..tostring() of Python 3.2
        # returns bytes (not string) regardless of the coding, while
        # tostring() of Python 3.1 returns a string.  To support both
        # cases transparently, we first make sure tostring() returns
        # bytes by specifying utf-8 and then convert the result to a
        # plain string (code below assume it).
        xml_string = str(xml.etree.ElementTree.tostring(xml_elem, encoding='utf-8'),
                         encoding='us-ascii')
465
466
        self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
            xml_string=xml_string,
467
            xsl_url_path=XSL_URL_PATH)
468
469
470
        assert self.xml_body is not None
        return self.xml_body

471
    def xsd_handler(self, module_name=None, item_name=None):
472
        """Handler which just returns the body of XSD document"""
473
474
475
476
477
478
479
480
481
482

        def stats_spec2xsd(stats_spec, xsd_elem):
            """Internal use for xsd_handler. Reads stats_spec
            specified as first arguments, and modify the xml object
            specified as second argument. xsd_elem must be
            modified. Always returns None with no exceptions."""
            # assumed module_spec or one stats_spec
            if type(stats_spec) is dict:
                # assumed module_spec
                if 'item_name' not in stats_spec:
483
                    for mod in stats_spec.keys():
484
485
486
487
                        elem = xml.etree.ElementTree.Element(
                            "element", { "name" : mod })
                        complextype = xml.etree.ElementTree.Element("complexType")
                        alltag = xml.etree.ElementTree.Element("all")
488
                        stats_spec2xsd(stats_spec[mod], alltag)
489
490
491
                        complextype.append(alltag)
                        elem.append(complextype)
                        xsd_elem.append(elem)
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
                # assumed stats_spec
                else:
                    if stats_spec['item_type'] == 'map':
                        alltag = xml.etree.ElementTree.Element("all")
                        stats_spec2xsd(stats_spec['map_item_spec'], alltag)
                        complextype = xml.etree.ElementTree.Element("complexType")
                        complextype.append(alltag)
                        elem = xml.etree.ElementTree.Element(
                            "element", { "name" : stats_spec["item_name"] })
                        elem.append(complextype)
                        xsd_elem.append(elem)
                    elif stats_spec['item_type'] == 'list':
                        alltag = xml.etree.ElementTree.Element("all")
                        stats_spec2xsd(stats_spec['list_item_spec'], alltag)
                        complextype = xml.etree.ElementTree.Element("complexType")
                        complextype.append(alltag)
                        elem = xml.etree.ElementTree.Element(
                            "element", { "name" : stats_spec["item_name"] })
                        elem.append(complextype)
                        xsd_elem.append(elem)
                    else:
                        elem = xml.etree.ElementTree.Element(
                            "element",
                            attrib={
                                'name' : stats_spec["item_name"],
                                'type' : stats_spec["item_type"] \
                                    if stats_spec["item_type"].lower() != 'real' \
                                    else 'float',
                                'minOccurs' : "1",
                                'maxOccurs' : "1"
                                }
                            )
                        annotation = xml.etree.ElementTree.Element("annotation")
                        appinfo = xml.etree.ElementTree.Element("appinfo")
                        documentation = xml.etree.ElementTree.Element("documentation")
                        if "item_title" in stats_spec:
                            appinfo.text = stats_spec["item_title"]
                        if "item_description" in stats_spec:
                            documentation.text = stats_spec["item_description"]
                        annotation.append(appinfo)
                        annotation.append(documentation)
                        elem.append(annotation)
                        xsd_elem.append(elem)
            # multiple stats_specs
            elif type(stats_spec) is list:
                for item_spec in stats_spec:
                    stats_spec2xsd(item_spec, xsd_elem)
            return None

Naoki Kambe's avatar
Naoki Kambe committed
541
        # for XSD
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
        stats_spec = self.get_stats_spec(module_name, item_name)
        alltag = xml.etree.ElementTree.Element("all")
        stats_spec2xsd(stats_spec, alltag)
        complextype = xml.etree.ElementTree.Element("complexType")
        complextype.append(alltag)
        documentation = xml.etree.ElementTree.Element("documentation")
        documentation.text = "A set of statistics data"
        annotation = xml.etree.ElementTree.Element("annotation")
        annotation.append(documentation)
        elem = xml.etree.ElementTree.Element(
            "element", attrib={ 'name' : 'statistics' })
        elem.append(annotation)
        elem.append(complextype)
        documentation = xml.etree.ElementTree.Element("documentation")
        documentation.text = "XML schema of the statistics data in BIND 10"
        annotation = xml.etree.ElementTree.Element("annotation")
        annotation.append(documentation)
        xsd_root = xml.etree.ElementTree.Element(
            "schema",
            attrib={ 'xmlns' : "http://www.w3.org/2001/XMLSchema",
                     'targetNamespace' : XSD_NAMESPACE,
                     'xmlns:bind10' : XSD_NAMESPACE })
        xsd_root.append(annotation)
        xsd_root.append(elem)
566
567
568
569
570
571
572
573
        # The coding conversion is tricky. xml..tostring() of Python 3.2
        # returns bytes (not string) regardless of the coding, while
        # tostring() of Python 3.1 returns a string.  To support both
        # cases transparently, we first make sure tostring() returns
        # bytes by specifying utf-8 and then convert the result to a
        # plain string (code below assume it).
        xsd_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
                         encoding='us-ascii')
Naoki Kambe's avatar
Naoki Kambe committed
574
        self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
575
            xsd_string=xsd_string)
Naoki Kambe's avatar
Naoki Kambe committed
576
        assert self.xsd_body is not None
577
        return self.xsd_body
Naoki Kambe's avatar
Naoki Kambe committed
578

579
    def xsl_handler(self, module_name=None, item_name=None):
580
        """Handler which just returns the body of XSL document"""
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614

        def stats_spec2xsl(stats_spec, xsl_elem):
            """Internal use for xsl_handler. Reads stats_spec
            specified as first arguments, and modify the xml object
            specified as second argument. xsl_elem must be
            modified. Always returns None with no exceptions."""
            # assumed module_spec or one stats_spec
            if type(stats_spec) is dict:
                # assumed module_spec
                if 'item_name' not in stats_spec:
                    table = xml.etree.ElementTree.Element("table")
                    tr = xml.etree.ElementTree.Element("tr")
                    th = xml.etree.ElementTree.Element("th")
                    th.text = "Module Names"
                    tr.append(th)
                    th = xml.etree.ElementTree.Element("th")
                    th.text = "Items"
                    tr.append(th)
                    table.append(tr)
                    for mod in stats_spec.keys():
                        foreach = xml.etree.ElementTree.Element(
                            "xsl:for-each", attrib={ "select" : mod })
                        tr = xml.etree.ElementTree.Element("tr")
                        td = xml.etree.ElementTree.Element("td")
                        td.text = mod
                        tr.append(td)
                        td = xml.etree.ElementTree.Element("td")
                        stats_spec2xsl(stats_spec[mod], td)
                        tr.append(td)
                        foreach.append(tr)
                        table.append(foreach)
                    xsl_elem.append(table)
                # assumed stats_spec
                else:
615
                    tr = xml.etree.ElementTree.Element("tr")
616
617
                    td = xml.etree.ElementTree.Element("td")
                    td.text = stats_spec["item_name"]
618
                    tr.append(td)
619
                    if stats_spec['item_type'] == 'map':
620
                        stats_spec2xsl(stats_spec['map_item_spec'], tr)
621
                    elif stats_spec['item_type'] == 'list':
622
                        stats_spec2xsl(stats_spec['list_item_spec'], tr)
623
                    else:
624
                        td = xml.etree.ElementTree.Element("td")
625
626
627
628
                        xsl_valueof = xml.etree.ElementTree.Element(
                            "xsl:value-of",
                            attrib={'select': stats_spec["item_name"]})
                        td.append(xsl_valueof)
629
630
                        tr.append(td)
                    xsl_elem.append(tr)
631
632
633
634
635
636
637
638
639
640
641
642
            # multiple stats_specs
            elif type(stats_spec) is list:
                table = xml.etree.ElementTree.Element("table")
                tr = xml.etree.ElementTree.Element("tr")
                th = xml.etree.ElementTree.Element("th")
                th.text = "Item Names"
                tr.append(th)
                th = xml.etree.ElementTree.Element("th")
                th.text = "Values"
                tr.append(th)
                table.append(tr)
                for item_spec in stats_spec:
643
                    stats_spec2xsl(item_spec, table)
644
645
646
                xsl_elem.append(table)
            return None

Naoki Kambe's avatar
Naoki Kambe committed
647
        # for XSL
648
649
        stats_spec = self.get_stats_spec(module_name, item_name)
        xsd_root = xml.etree.ElementTree.Element( # started with xml:template tag
Naoki Kambe's avatar
Naoki Kambe committed
650
            "xsl:template",
651
            attrib={'match': "bind10:statistics"})
652
653
654
655
        if module_name is not None and item_name is not None:
            stats_spec2xsl([ stats_spec ] , xsd_root)
        else:
            stats_spec2xsl(stats_spec, xsd_root)
656
657
658
659
660
661
662
663
        # The coding conversion is tricky. xml..tostring() of Python 3.2
        # returns bytes (not string) regardless of the coding, while
        # tostring() of Python 3.1 returns a string.  To support both
        # cases transparently, we first make sure tostring() returns
        # bytes by specifying utf-8 and then convert the result to a
        # plain string (code below assume it).
        xsl_string = str(xml.etree.ElementTree.tostring(xsd_root, encoding='utf-8'),
                         encoding='us-ascii')
Naoki Kambe's avatar
Naoki Kambe committed
664
665
666
667
668
669
670
        self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
            xsl_string=xsl_string,
            xsd_namespace=XSD_NAMESPACE)
        assert self.xsl_body is not None
        return self.xsl_body

    def open_template(self, file_name):
671
672
673
        """It opens a template file, and it loads all lines to a
        string variable and returns string. Template object includes
        the variable. Limitation of a file size isn't needed there."""
674
675
676
        f = open(file_name, 'r')
        lines = "".join(f.readlines())
        f.close()
Naoki Kambe's avatar
Naoki Kambe committed
677
678
679
680
681
682
683
684
685
686
        assert lines is not None
        return string.Template(lines)

if __name__ == "__main__":
    try:
        parser = OptionParser()
        parser.add_option(
            "-v", "--verbose", dest="verbose", action="store_true",
            help="display more about what is going on")
        (options, args) = parser.parse_args()
687
688
689
        if options.verbose:
            isc.log.init("b10-stats-httpd", "DEBUG", 99)
        stats_httpd = StatsHttpd()
Naoki Kambe's avatar
Naoki Kambe committed
690
        stats_httpd.start()
691
    except OptionValueError as ove:
692
        logger.fatal(STATHTTPD_BAD_OPTION_VALUE, ove)
693
        sys.exit(1)
Naoki Kambe's avatar
Naoki Kambe committed
694
    except isc.cc.session.SessionError as se:
695
        logger.fatal(STATHTTPD_CC_SESSION_ERROR, se)
696
        sys.exit(1)
Naoki Kambe's avatar
Naoki Kambe committed
697
    except HttpServerError as hse:
Naoki Kambe's avatar
Naoki Kambe committed
698
        logger.fatal(STATHTTPD_START_SERVER_INIT_ERROR, hse)
699
        sys.exit(1)
Naoki Kambe's avatar
Naoki Kambe committed
700
    except KeyboardInterrupt as kie:
701
        logger.info(STATHTTPD_STOPPED_BY_KEYBOARD)