From a063883897d83db47451f2c870ecb8d899c81dd0 Mon Sep 17 00:00:00 2001 From: Oleksiy Mishchenko <oleksiy@google.com> Date: Thu, 24 Jul 2008 16:34:49 +0000 Subject: [PATCH] Switch RAPI to ganeti.http module Reviewed-by: imsnah --- Makefile.am | 1 + lib/http.py | 1 + lib/rapi/RESTHTTPServer.py | 221 ++++--------------------- lib/rapi/connector.py | 6 +- test/ganeti.http_unittest.py | 84 ++++++++++ test/ganeti.rapi.resources_unittest.py | 63 +------ 6 files changed, 122 insertions(+), 254 deletions(-) create mode 100755 test/ganeti.http_unittest.py diff --git a/Makefile.am b/Makefile.am index 07332a8e0..7507f8514 100644 --- a/Makefile.am +++ b/Makefile.am @@ -195,6 +195,7 @@ dist_TESTS = \ test/ganeti.serializer_unittest.py \ test/ganeti.workerpool_unittest.py \ test/ganeti.rapi.resources_unittest.py \ + test/ganeti.http_unittest.py \ test/ganeti.constants_unittest.py nodist_TESTS = diff --git a/lib/http.py b/lib/http.py index a9febda66..8f78d57d8 100644 --- a/lib/http.py +++ b/lib/http.py @@ -108,6 +108,7 @@ class ApacheLogfile: # Message format % args, )) + self._fd.flush() def _FormatCurrentTime(self): """Formats current time in Common Log Format. diff --git a/lib/rapi/RESTHTTPServer.py b/lib/rapi/RESTHTTPServer.py index bbea1a446..11c83b788 100644 --- a/lib/rapi/RESTHTTPServer.py +++ b/lib/rapi/RESTHTTPServer.py @@ -20,217 +20,56 @@ """ -import BaseHTTPServer -import OpenSSL -import re -import socket -import time - from ganeti import constants +from ganeti import http from ganeti import errors -from ganeti import logger from ganeti import rpc -from ganeti import serializer - from ganeti.rapi import connector from ganeti.rapi import httperror -class HttpLogfile: - """Utility class to write HTTP server log files. - - The written format is the "Common Log Format" as defined by Apache: - http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples - - """ - MONTHNAME = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - def __init__(self, path): - self._fd = open(path, 'a', 1) - - def __del__(self): - try: - self.Close() - except: - # Swallow exceptions - pass - - def Close(self): - if self._fd is not None: - self._fd.close() - self._fd = None - - def LogRequest(self, request, format, *args): - if self._fd is None: - raise errors.ProgrammerError("Logfile already closed") - - request_time = self._FormatCurrentTime() - - self._fd.write("%s %s %s [%s] %s\n" % ( - # Remote host address - request.address_string(), - - # RFC1413 identity (identd) - "-", - - # Remote user - "-", - - # Request time - request_time, - - # Message - format % args, - )) - - def _FormatCurrentTime(self): - """Formats current time in Common Log Format. - - """ - return self._FormatLogTime(time.time()) - - def _FormatLogTime(self, seconds): - """Formats time for Common Log Format. - - All timestamps are logged in the UTC timezone. - - Args: - - seconds: Time in seconds since the epoch - - """ - (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds) - format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000" - return time.strftime(format, tm) - - -class RESTHTTPServer(BaseHTTPServer.HTTPServer): - """Class to provide an HTTP/HTTPS server. - - """ - allow_reuse_address = True - - def __init__(self, server_address, HandlerClass, options): - """REST Server Constructor. - - Args: - server_address: a touple containing: - ip: a string with IP address, localhost if empty string - port: port number, integer - HandlerClass: HTTPRequestHandler object - options: Command-line options - - """ - logger.SetupLogging(debug=options.debug, program='ganeti-rapi') - - self.httplog = HttpLogfile(constants.LOG_RAPIACCESS) - - BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass) - if options.ssl: - # Set up SSL - context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) - context.use_privatekey_file(options.ssl_key) - context.use_certificate_file(options.ssl_cert) - self.socket = OpenSSL.SSL.Connection(context, - socket.socket(self.address_family, - self.socket_type)) - else: - self.socket = socket.socket(self.address_family, self.socket_type) - - self.server_bind() - self.server_activate() - - -class JsonResponse: - CONTENT_TYPE = "application/json" - - def Encode(self, data): - return serializer.DumpJson(data) - - -class RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class RESTRequestHandler(http.HTTPRequestHandler): """REST Request Handler Class. """ def setup(self): - """Setup secure read and write file objects. - - """ - self.connection = self.request - self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) - self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + super(RESTRequestHandler, self).setup() self._resmap = connector.Mapper() - - def handle_one_request(self): - """Handle a single REST request. + + def HandleRequest(self): + """ Handels a request. """ - self.raw_requestline = None - try: - self.raw_requestline = self.rfile.readline() - except OpenSSL.SSL.Error, ex: - logger.Error("Error in SSL: %s" % str(ex)) - if not self.raw_requestline: - self.close_connection = 1 - return - if not self.parse_request(): # An error code has been sent, just exit - return + (HandlerClass, items, args) = self._resmap.getController(self.path) + handler = HandlerClass(self, items, args) + command = self.command.upper() try: - (HandlerClass, items, args) = self._resmap.getController(self.path) - handler = HandlerClass(self, items, args) + fn = getattr(handler, command) + except AttributeError, err: + raise httperror.HTTPBadRequest() - command = self.command.upper() - try: - fn = getattr(handler, command) - except AttributeError, err: - raise httperror.HTTPBadRequest() - - try: - result = fn() - - except errors.OpPrereqError, err: - # TODO: "Not found" is not always the correct error. Ganeti's core must - # differentiate between different error types. - raise httperror.HTTPNotFound(message=str(err)) - - encoder = JsonResponse() - encoded_result = encoder.Encode(result) - - self.send_response(200) - self.send_header("Content-Type", encoder.CONTENT_TYPE) - self.end_headers() - self.wfile.write(encoded_result) - - except httperror.HTTPException, err: - self.send_error(err.code, message=err.message) - - except Exception, err: - self.send_error(httperror.HTTPInternalError.code, message=str(err)) - - def log_message(self, format, *args): - """Log an arbitrary message. - - This is used by all other logging functions. - - The first argument, FORMAT, is a format string for the - message to be logged. If the format string contains - any % escapes requiring parameters, they should be - specified as subsequent arguments (it's just like - printf!). + try: + result = fn() - """ - self.server.httplog.LogRequest(self, format, *args) + except errors.OpPrereqError, err: + # TODO: "Not found" is not always the correct error. Ganeti's core must + # differentiate between different error types. + raise httperror.HTTPNotFound(message=str(err)) + + return result def start(options): - # Disable signal handlers, otherwise we can't exit the daemon in a clean way - # by sending a signal. - rpc.install_twisted_signal_handlers = False - - httpd = RESTHTTPServer(("", options.port), RESTRequestHandler, options) + log_fd = open(constants.LOG_RAPIACCESS, 'a') try: - httpd.serve_forever() + apache_log = http.ApacheLogfile(log_fd) + httpd = http.HTTPServer(("", options.port), RESTRequestHandler, + httplog=apache_log) + try: + httpd.serve_forever() + finally: + httpd.server_close() + finally: - httpd.server_close() + log_fd.close() diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py index 782065469..a80250cce 100644 --- a/lib/rapi/connector.py +++ b/lib/rapi/connector.py @@ -26,9 +26,9 @@ import cgi import re from ganeti import constants +from ganeti import http from ganeti.rapi import baserlib -from ganeti.rapi import httperror from ganeti.rapi import rlib1 from ganeti.rapi import rlib2 @@ -85,10 +85,10 @@ class Mapper: result = (handler, [], args) break - if result is not None: + if result: return result else: - raise httperror.HTTPNotFound() + raise http.HTTPNotFound() class R_root(baserlib.R_Generic): diff --git a/test/ganeti.http_unittest.py b/test/ganeti.http_unittest.py new file mode 100755 index 000000000..448fca25b --- /dev/null +++ b/test/ganeti.http_unittest.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# + +# Copyright (C) 2007, 2008 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Script for unittesting the http module""" + + +import os +import unittest +import tempfile +import time + +from ganeti import http + + +class HttpLogfileTests(unittest.TestCase): + """Rests for ApacheLogfile class.""" + + class FakeRequest: + FAKE_ADDRESS = "1.2.3.4" + + def address_string(self): + return self.FAKE_ADDRESS + + def setUp(self): + self.tmpfile = tempfile.NamedTemporaryFile() + self.logfile = http.ApacheLogfile(self.tmpfile) + + def tearDown(self): + self.tmpfile.close() + + def testFormatLogTime(self): + self._TestInTimezone(1208646123.0, "Europe/London", + "19/Apr/2008:23:02:03 +0000") + self._TestInTimezone(1208646123, "Europe/Zurich", + "19/Apr/2008:23:02:03 +0000") + self._TestInTimezone(1208646123, "Australia/Sydney", + "19/Apr/2008:23:02:03 +0000") + + def _TestInTimezone(self, seconds, timezone, expected): + """Tests HttpLogfile._FormatLogTime with a specific timezone + + """ + # Preserve environment + old_TZ = os.environ.get("TZ", None) + try: + os.environ["TZ"] = timezone + time.tzset() + result = self.logfile._FormatLogTime(seconds) + finally: + # Restore environment + if old_TZ is not None: + os.environ["TZ"] = old_TZ + elif "TZ" in os.environ: + del os.environ["TZ"] + time.tzset() + + self.assertEqual(result, expected) + + + def testLogRequest(self): + request = self.FakeRequest() + self.logfile.LogRequest(request, "This is only a %s", "test") + + +if __name__ == '__main__': + unittest.main() diff --git a/test/ganeti.rapi.resources_unittest.py b/test/ganeti.rapi.resources_unittest.py index 461cfe9a7..1385eab4e 100755 --- a/test/ganeti.rapi.resources_unittest.py +++ b/test/ganeti.rapi.resources_unittest.py @@ -22,14 +22,13 @@ """Script for unittesting the RAPI resources module""" -import os import unittest import tempfile -import time from ganeti import errors +from ganeti import http + from ganeti.rapi import connector -from ganeti.rapi import httperror from ganeti.rapi import RESTHTTPServer from ganeti.rapi import rlib1 @@ -44,7 +43,7 @@ class MapperTests(unittest.TestCase): self.assertEquals(self.map.getController(uri), result) def _TestFailingUri(self, uri): - self.failUnlessRaises(httperror.HTTPNotFound, self.map.getController, uri) + self.failUnlessRaises(http.HTTPNotFound, self.map.getController, uri) def testMapper(self): """Testing Mapper""" @@ -86,61 +85,5 @@ class R_RootTests(unittest.TestCase): self.assertEquals(self.root.GET(), expected) -class HttpLogfileTests(unittest.TestCase): - """Rests for HttpLogfile class.""" - - class FakeRequest: - FAKE_ADDRESS = "1.2.3.4" - - def address_string(self): - return self.FAKE_ADDRESS - - def setUp(self): - self.tmpfile = tempfile.NamedTemporaryFile() - self.logfile = RESTHTTPServer.HttpLogfile(self.tmpfile.name) - - def testFormatLogTime(self): - self._TestInTimezone(1208646123.0, "Europe/London", - "19/Apr/2008:23:02:03 +0000") - self._TestInTimezone(1208646123, "Europe/Zurich", - "19/Apr/2008:23:02:03 +0000") - self._TestInTimezone(1208646123, "Australia/Sydney", - "19/Apr/2008:23:02:03 +0000") - - def _TestInTimezone(self, seconds, timezone, expected): - """Tests HttpLogfile._FormatLogTime with a specific timezone - - """ - # Preserve environment - old_TZ = os.environ.get("TZ", None) - try: - os.environ["TZ"] = timezone - time.tzset() - result = self.logfile._FormatLogTime(seconds) - finally: - # Restore environment - if old_TZ is not None: - os.environ["TZ"] = old_TZ - elif "TZ" in os.environ: - del os.environ["TZ"] - time.tzset() - - self.assertEqual(result, expected) - - def testClose(self): - self.logfile.Close() - - def testCloseAndWrite(self): - request = self.FakeRequest() - self.logfile.Close() - self.assertRaises(errors.ProgrammerError, self.logfile.LogRequest, - request, "Message") - - def testLogRequest(self): - request = self.FakeRequest() - self.logfile.LogRequest(request, "This is only a %s", "test") - self.logfile.Close() - - if __name__ == '__main__': unittest.main() -- GitLab