Code and docstring style fixes
[ganeti-local] / lib / http / server.py
index fdaf6ab..d7e374c 100644 (file)
@@ -30,10 +30,8 @@ import select
 import socket
 import time
 import signal
+import asyncore
 
-from ganeti import constants
-from ganeti import serializer
-from ganeti import utils
 from ganeti import http
 
 
@@ -59,11 +57,15 @@ DEFAULT_ERROR_MESSAGE = """\
 """
 
 
-def _DateTimeHeader():
+def _DateTimeHeader(gmnow=None):
   """Return the current date and time formatted for a message header.
 
+  The time MUST be in the GMT timezone.
+
   """
-  (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
+  if gmnow is None:
+    gmnow = time.gmtime()
+  (year, month, day, hh, mm, ss, wd, _, _) = gmnow
   return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
           (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
 
@@ -82,13 +84,27 @@ class _HttpServerRequest(object):
     # Response attributes
     self.resp_headers = {}
 
+    # Private data for request handler (useful in combination with
+    # authentication)
+    self.private = None
+
 
 class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
   """Writes an HTTP response to client.
 
   """
   def __init__(self, sock, request_msg, response_msg, write_timeout):
-    """TODO
+    """Writes the response to the client.
+
+    @type sock: socket
+    @param sock: Target socket
+    @type request_msg: http.HttpMessage
+    @param request_msg: Request message, required to determine whether
+        response may have a message body
+    @type response_msg: http.HttpMessage
+    @param response_msg: Response message
+    @type write_timeout: float
+    @param write_timeout: Write timeout for socket
 
     """
     self._request_msg = request_msg
@@ -120,9 +136,11 @@ class _HttpServerToClientMessageWriter(http.HttpMessageWriter):
     # message-body, [...]"
 
     return (http.HttpMessageWriter.HasMessageBody(self) and
-            (request_method is not None and request_method != http.HTTP_HEAD) and
+            (request_method is not None and
+             request_method != http.HTTP_HEAD) and
             response_code >= http.HTTP_OK and
-            response_code not in (http.HTTP_NO_CONTENT, http.HTTP_NOT_MODIFIED))
+            response_code not in (http.HTTP_NO_CONTENT,
+                                  http.HTTP_NOT_MODIFIED))
 
 
 class _HttpClientToServerMessageReader(http.HttpMessageReader):
@@ -187,12 +205,13 @@ class _HttpClientToServerMessageReader(http.HttpMessageReader):
     return http.HttpClientToServerStartLine(method, path, version)
 
 
-class _HttpServerRequestExecutor(object):
+class HttpServerRequestExecutor(object):
   """Implements server side of HTTP.
 
-  This class implements the server side of HTTP. It's based on code of Python's
-  BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
-  character encodings. Keep-alive connections are not supported.
+  This class implements the server side of HTTP. It's based on code of
+  Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
+  support non-ASCII character encodings. Keep-alive connections are
+  not supported.
 
   """
   # The default request version.  This only affects responses up until
@@ -220,8 +239,6 @@ class _HttpServerRequestExecutor(object):
     self.sock = sock
     self.client_addr = client_addr
 
-    self.poller = select.poll()
-
     self.request_msg = http.HttpMessage()
     self.response_msg = http.HttpMessage()
 
@@ -235,11 +252,20 @@ class _HttpServerRequestExecutor(object):
     # Operate in non-blocking mode
     self.sock.setblocking(0)
 
-    logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
+    logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
     try:
       request_msg_reader = None
       force_close = True
       try:
+        # Do the secret SSL handshake
+        if self.server.using_ssl:
+          self.sock.set_accept_state()
+          try:
+            http.Handshake(self.sock, self.WRITE_TIMEOUT)
+          except http.HttpSessionHandshakeUnexpectedEOF:
+            # Ignore rest
+            return
+
         try:
           try:
             request_msg_reader = self._ReadRequest()
@@ -253,14 +279,13 @@ class _HttpServerRequestExecutor(object):
           # Try to send a response
           self._SendResponse()
       finally:
-        http.ShutdownConnection(self.poller, sock,
-                                self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
+        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
                                 request_msg_reader, force_close)
 
       self.sock.close()
       self.sock = None
     finally:
-      logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
+      logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
 
   def _ReadRequest(self):
     """Reads a request sent by client.
@@ -286,22 +311,30 @@ class _HttpServerRequestExecutor(object):
     handler_context = _HttpServerRequest(self.request_msg)
 
     try:
-      result = self.server.HandleRequest(handler_context)
-    except (http.HttpException, KeyboardInterrupt, SystemExit):
-      raise
-    except Exception, err:
-      logging.exception("Caught exception")
-      raise http.HttpInternalError(message=str(err))
-    except:
-      logging.exception("Unknown exception")
-      raise http.HttpInternalError(message="Unknown error")
-
-    # TODO: Content-type
-    encoder = http.HttpJsonConverter()
-    self.response_msg.start_line.code = http.HTTP_OK
-    self.response_msg.body = encoder.Encode(result)
-    self.response_msg.headers = handler_context.resp_headers
-    self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
+      try:
+        # Authentication, etc.
+        self.server.PreHandleRequest(handler_context)
+
+        # Call actual request handler
+        result = self.server.HandleRequest(handler_context)
+      except (http.HttpException, KeyboardInterrupt, SystemExit):
+        raise
+      except Exception, err:
+        logging.exception("Caught exception")
+        raise http.HttpInternalServerError(message=str(err))
+      except:
+        logging.exception("Unknown exception")
+        raise http.HttpInternalServerError(message="Unknown error")
+
+      # TODO: Content-type
+      encoder = http.HttpJsonConverter()
+      self.response_msg.start_line.code = http.HTTP_OK
+      self.response_msg.body = encoder.Encode(result)
+      self.response_msg.headers = handler_context.resp_headers
+      self.response_msg.headers[http.HTTP_CONTENT_TYPE] = encoder.CONTENT_TYPE
+    finally:
+      # No reason to keep this any longer, even for exceptions
+      handler_context.private = None
 
   def _SendResponse(self):
     """Sends the response to the client.
@@ -363,13 +396,27 @@ class _HttpServerRequestExecutor(object):
       }
 
     self.response_msg.start_line.code = err.code
-    self.response_msg.headers = {
-      http.HTTP_CONTENT_TYPE: self.error_content_type,
-      }
-    self.response_msg.body = self.error_message_format % values
 
+    headers = {}
+    if err.headers:
+      headers.update(err.headers)
+    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
+    self.response_msg.headers = headers
+
+    self.response_msg.body = self._FormatErrorMessage(values)
+
+  def _FormatErrorMessage(self, values):
+    """Formats the body of an error message.
+
+    @type values: dict
+    @param values: dictionary with keys code, message and explain.
+    @rtype: string
+    @return: the body of the message
 
-class HttpServer(http.HttpSocketBase):
+    """
+    return self.error_message_format % values
+
+class HttpServer(http.HttpBase, asyncore.dispatcher):
   """Generic HTTP server class
 
   Users of this class must subclass it and override the HandleRequest function.
@@ -378,23 +425,33 @@ class HttpServer(http.HttpSocketBase):
   MAX_CHILDREN = 20
 
   def __init__(self, mainloop, local_address, port,
-               ssl_params=None, ssl_verify_peer=False):
+               ssl_params=None, ssl_verify_peer=False,
+               request_executor_class=None):
     """Initializes the HTTP server
 
     @type mainloop: ganeti.daemon.Mainloop
     @param mainloop: Mainloop used to poll for I/O events
-    @type local_addess: string
+    @type local_address: string
     @param local_address: Local IP address to bind to
     @type port: int
     @param port: TCP port to listen on
     @type ssl_params: HttpSslParams
     @param ssl_params: SSL key and certificate
     @type ssl_verify_peer: bool
-    @param ssl_verify_peer: Whether to require client certificate and compare
-                            it with our certificate
+    @param ssl_verify_peer: Whether to require client certificate
+        and compare it with our certificate
+    @type request_executor_class: class
+    @param request_executor_class: an class derived from the
+        HttpServerRequestExecutor class
 
     """
-    http.HttpSocketBase.__init__(self)
+    http.HttpBase.__init__(self)
+    asyncore.dispatcher.__init__(self)
+
+    if request_executor_class is None:
+      self.request_executor = HttpServerRequestExecutor
+    else:
+      self.request_executor = request_executor_class
 
     self.mainloop = mainloop
     self.local_address = local_address
@@ -406,8 +463,8 @@ class HttpServer(http.HttpSocketBase):
     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
     self._children = []
-
-    mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
+    self.set_socket(self.socket)
+    self.accepting = True
     mainloop.RegisterSignal(self)
 
   def Start(self):
@@ -417,9 +474,8 @@ class HttpServer(http.HttpSocketBase):
   def Stop(self):
     self.socket.close()
 
-  def OnIO(self, fd, condition):
-    if condition & select.POLLIN:
-      self._IncomingConnection()
+  def handle_accept(self):
+    self._IncomingConnection()
 
   def OnSignal(self, signum):
     if signum == signal.SIGCHLD:
@@ -440,7 +496,7 @@ class HttpServer(http.HttpSocketBase):
           # As soon as too many children run, we'll not respond to new
           # requests. The real solution would be to add a timeout for children
           # and killing them after some time.
-          pid, status = os.waitpid(0, 0)
+          pid, _ = os.waitpid(0, 0)
         except os.error:
           pid = None
         if pid and pid in self._children:
@@ -466,7 +522,17 @@ class HttpServer(http.HttpSocketBase):
     if pid == 0:
       # Child process
       try:
-        _HttpServerRequestExecutor(self, connection, client_addr)
+        # The client shouldn't keep the listening socket open. If the parent
+        # process is restarted, it would fail when there's already something
+        # listening (in this case its own child from a previous run) on the
+        # same port.
+        try:
+          self.socket.close()
+        except socket.error:
+          pass
+        self.socket = None
+
+        self.request_executor(self, connection, client_addr)
       except Exception:
         logging.exception("Error while handling request from %s:%s",
                           client_addr[0], client_addr[1])
@@ -475,10 +541,17 @@ class HttpServer(http.HttpSocketBase):
     else:
       self._children.append(pid)
 
+  def PreHandleRequest(self, req):
+    """Called before handling a request.
+
+    Can be overridden by a subclass.
+
+    """
+
   def HandleRequest(self, req):
     """Handles a request.
 
-    Must be overriden by subclass.
+    Must be overridden by subclass.
 
     """
     raise NotImplementedError()