Convert gnt-instance info to the hvparams model
[ganeti-local] / lib / http.py
1 #
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
16 # 02110-1301, USA.
17
18 """HTTP server module.
19
20 """
21
22 import BaseHTTPServer
23 import cgi
24 import logging
25 import mimetools
26 import OpenSSL
27 import os
28 import select
29 import socket
30 import sys
31 import time
32 import signal
33
34 from ganeti import constants
35 from ganeti import logger
36 from ganeti import serializer
37
38
39 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
40 MONTHNAME = [None,
41              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
42              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
43
44 # Default error message
45 DEFAULT_ERROR_CONTENT_TYPE = "text/html"
46 DEFAULT_ERROR_MESSAGE = """\
47 <head>
48 <title>Error response</title>
49 </head>
50 <body>
51 <h1>Error response</h1>
52 <p>Error code %(code)d.
53 <p>Message: %(message)s.
54 <p>Error code explanation: %(code)s = %(explain)s.
55 </body>
56 """
57
58 HTTP_OK = 200
59 HTTP_NO_CONTENT = 204
60 HTTP_NOT_MODIFIED = 304
61
62 HTTP_0_9 = "HTTP/0.9"
63 HTTP_1_0 = "HTTP/1.0"
64 HTTP_1_1 = "HTTP/1.1"
65
66 HTTP_GET = "GET"
67 HTTP_HEAD = "HEAD"
68
69
70 class SocketClosed(socket.error):
71   pass
72
73
74 class HTTPException(Exception):
75   code = None
76   message = None
77
78   def __init__(self, message=None):
79     Exception.__init__(self)
80     if message is not None:
81       self.message = message
82
83
84 class HTTPBadRequest(HTTPException):
85   code = 400
86
87
88 class HTTPForbidden(HTTPException):
89   code = 403
90
91
92 class HTTPNotFound(HTTPException):
93   code = 404
94
95
96 class HTTPGone(HTTPException):
97   code = 410
98
99
100 class HTTPLengthRequired(HTTPException):
101   code = 411
102
103
104 class HTTPInternalError(HTTPException):
105   code = 500
106
107
108 class HTTPNotImplemented(HTTPException):
109   code = 501
110
111
112 class HTTPServiceUnavailable(HTTPException):
113   code = 503
114
115
116 class HTTPVersionNotSupported(HTTPException):
117   code = 505
118
119
120 class ApacheLogfile:
121   """Utility class to write HTTP server log files.
122
123   The written format is the "Common Log Format" as defined by Apache:
124   http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
125
126   """
127   def __init__(self, fd):
128     """Constructor for ApacheLogfile class.
129
130     Args:
131     - fd: Open file object
132
133     """
134     self._fd = fd
135
136   def LogRequest(self, request, format, *args):
137     self._fd.write("%s %s %s [%s] %s\n" % (
138       # Remote host address
139       request.address_string(),
140
141       # RFC1413 identity (identd)
142       "-",
143
144       # Remote user
145       "-",
146
147       # Request time
148       self._FormatCurrentTime(),
149
150       # Message
151       format % args,
152       ))
153     self._fd.flush()
154
155   def _FormatCurrentTime(self):
156     """Formats current time in Common Log Format.
157
158     """
159     return self._FormatLogTime(time.time())
160
161   def _FormatLogTime(self, seconds):
162     """Formats time for Common Log Format.
163
164     All timestamps are logged in the UTC timezone.
165
166     Args:
167     - seconds: Time in seconds since the epoch
168
169     """
170     (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
171     format = "%d/" + MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
172     return time.strftime(format, tm)
173
174
175 class HTTPServer(BaseHTTPServer.HTTPServer, object):
176   """Class to provide an HTTP/HTTPS server.
177
178   """
179   allow_reuse_address = True
180
181   def __init__(self, server_address, HandlerClass, httplog=None,
182                enable_ssl=False, ssl_key=None, ssl_cert=None):
183     """Server constructor.
184
185     Args:
186       server_address: a touple containing:
187         ip: a string with IP address, localhost if empty string
188         port: port number, integer
189       HandlerClass: HTTPRequestHandler object
190       httplog: Access log object
191       enable_ssl: Whether to enable SSL
192       ssl_key: SSL key file
193       ssl_cert: SSL certificate key
194
195     """
196     BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
197
198     self.httplog = httplog
199
200     if enable_ssl:
201       # Set up SSL
202       context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
203       context.use_privatekey_file(ssl_key)
204       context.use_certificate_file(ssl_cert)
205       self.socket = OpenSSL.SSL.Connection(context,
206                                            socket.socket(self.address_family,
207                                            self.socket_type))
208     else:
209       self.socket = socket.socket(self.address_family, self.socket_type)
210
211     self.server_bind()
212     self.server_activate()
213
214
215 class HTTPJsonConverter:
216   CONTENT_TYPE = "application/json"
217
218   def Encode(self, data):
219     return serializer.DumpJson(data)
220
221   def Decode(self, data):
222     return serializer.LoadJson(data)
223
224
225 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
226   """Request handler class.
227
228   """
229   def setup(self):
230     """Setup secure read and write file objects.
231
232     """
233     self.connection = self.request
234     self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
235     self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
236
237   def handle_one_request(self):
238     """Parses a request and calls the handler function.
239
240     """
241     self.raw_requestline = None
242     try:
243       self.raw_requestline = self.rfile.readline()
244     except OpenSSL.SSL.Error, ex:
245       logger.Error("Error in SSL: %s" % str(ex))
246     if not self.raw_requestline:
247       self.close_connection = 1
248       return
249     if not self.parse_request(): # An error code has been sent, just exit
250       return
251     logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n"))
252
253     try:
254       self._ReadPostData()
255
256       result = self.HandleRequest()
257
258       # TODO: Content-type
259       encoder = HTTPJsonConverter()
260       encoded_result = encoder.Encode(result)
261
262       self.send_response(200)
263       self.send_header("Content-Type", encoder.CONTENT_TYPE)
264       self.send_header("Content-Length", str(len(encoded_result)))
265       self.end_headers()
266
267       self.wfile.write(encoded_result)
268
269     except HTTPException, err:
270       self.send_error(err.code, message=err.message)
271
272     except Exception, err:
273       self.send_error(HTTPInternalError.code, message=str(err))
274
275     except:
276       self.send_error(HTTPInternalError.code, message="Unknown error")
277
278   def _ReadPostData(self):
279     if self.command.upper() not in ("POST", "PUT"):
280       self.post_data = None
281       return
282
283     # TODO: Decide what to do when Content-Length header was not sent
284     try:
285       content_length = int(self.headers.get('Content-Length', 0))
286     except ValueError:
287       raise HTTPBadRequest("No Content-Length header or invalid format")
288
289     try:
290       data = self.rfile.read(content_length)
291     except socket.error, err:
292       logger.Error("Socket error while reading: %s" % str(err))
293       return
294
295     # TODO: Content-type, error handling
296     self.post_data = HTTPJsonConverter().Decode(data)
297
298     logging.debug("HTTP POST data: %s", self.post_data)
299
300   def HandleRequest(self):
301     """Handles a request.
302
303     """
304     raise NotImplementedError()
305
306   def log_message(self, format, *args):
307     """Log an arbitrary message.
308
309     This is used by all other logging functions.
310
311     The first argument, FORMAT, is a format string for the
312     message to be logged.  If the format string contains
313     any % escapes requiring parameters, they should be
314     specified as subsequent arguments (it's just like
315     printf!).
316
317     """
318     logging.debug("Handled request: %s", format % args)
319     if self.server.httplog:
320       self.server.httplog.LogRequest(self, format, *args)
321
322
323 class _HttpConnectionHandler(object):
324   """Implements server side of HTTP
325
326   This class implements the server side of HTTP. It's based on code of Python's
327   BaseHTTPServer, from both version 2.4 and 3k. It does not support non-ASCII
328   character encodings. Keep-alive connections are not supported.
329
330   """
331   # String for "Server" header
332   server_version = "Ganeti %s" % constants.RELEASE_VERSION
333
334   # The default request version.  This only affects responses up until
335   # the point where the request line is parsed, so it mainly decides what
336   # the client gets back when sending a malformed request line.
337   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
338   default_request_version = HTTP_0_9
339
340   # Error message settings
341   error_message_format = DEFAULT_ERROR_MESSAGE
342   error_content_type = DEFAULT_ERROR_CONTENT_TYPE
343
344   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
345
346   def __init__(self, server, conn, client_addr, fileio_class):
347     """Initializes this class.
348
349     Part of the initialization is reading the request and eventual POST/PUT
350     data sent by the client.
351
352     """
353     self._server = server
354
355     # We default rfile to buffered because otherwise it could be
356     # really slow for large data (a getc() call per byte); we make
357     # wfile unbuffered because (a) often after a write() we want to
358     # read and we need to flush the line; (b) big writes to unbuffered
359     # files are typically optimized by stdio even when big reads
360     # aren't.
361     self.rfile = fileio_class(conn, mode="rb", bufsize=-1)
362     self.wfile = fileio_class(conn, mode="wb", bufsize=0)
363
364     self.client_addr = client_addr
365
366     self.request_headers = None
367     self.request_method = None
368     self.request_path = None
369     self.request_requestline = None
370     self.request_version = self.default_request_version
371
372     self.response_body = None
373     self.response_code = HTTP_OK
374     self.response_content_type = None
375
376     self.should_fork = False
377
378     try:
379       self._ReadRequest()
380       self._ReadPostData()
381
382       self.should_fork = self._server.ForkForRequest(self)
383     except HTTPException, err:
384       self._SetErrorStatus(err)
385
386   def Close(self):
387     if not self.wfile.closed:
388       self.wfile.flush()
389     self.wfile.close()
390     self.rfile.close()
391
392   def _DateTimeHeader(self):
393     """Return the current date and time formatted for a message header.
394
395     """
396     (year, month, day, hh, mm, ss, wd, _, _) = time.gmtime()
397     return ("%s, %02d %3s %4d %02d:%02d:%02d GMT" %
398             (WEEKDAYNAME[wd], day, MONTHNAME[month], year, hh, mm, ss))
399
400   def _SetErrorStatus(self, err):
401     """Sets the response code and body from a HTTPException.
402
403     @type err: HTTPException
404     @param err: Exception instance
405
406     """
407     try:
408       (shortmsg, longmsg) = self.responses[err.code]
409     except KeyError:
410       shortmsg = longmsg = "Unknown"
411
412     if err.message:
413       message = err.message
414     else:
415       message = shortmsg
416
417     values = {
418       "code": err.code,
419       "message": cgi.escape(message),
420       "explain": longmsg,
421       }
422
423     self.response_code = err.code
424     self.response_content_type = self.error_content_type
425     self.response_body = self.error_message_format % values
426
427   def HandleRequest(self):
428     """Handle the actual request.
429
430     Calls the actual handler function and converts exceptions into HTTP errors.
431
432     """
433     # Don't do anything if there's already been a problem
434     if self.response_code != HTTP_OK:
435       return
436
437     assert self.request_method, "Status code %s requires a method" % HTTP_OK
438
439     # Check whether client is still there
440     self.rfile.read(0)
441
442     try:
443       try:
444         result = self._server.HandleRequest(self)
445
446         # TODO: Content-type
447         encoder = HTTPJsonConverter()
448         body = encoder.Encode(result)
449
450         self.response_content_type = encoder.CONTENT_TYPE
451         self.response_body = body
452       except (HTTPException, KeyboardInterrupt, SystemExit):
453         raise
454       except Exception, err:
455         logging.exception("Caught exception")
456         raise HTTPInternalError(message=str(err))
457       except:
458         logging.exception("Unknown exception")
459         raise HTTPInternalError(message="Unknown error")
460
461     except HTTPException, err:
462       self._SetErrorStatus(err)
463
464   def SendResponse(self):
465     """Sends response to the client.
466
467     """
468     # Check whether client is still there
469     self.rfile.read(0)
470
471     logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
472                  self.request_requestline, self.response_code)
473
474     if self.response_code in self.responses:
475       response_message = self.responses[self.response_code][0]
476     else:
477       response_message = ""
478
479     if self.request_version != HTTP_0_9:
480       self.wfile.write("%s %d %s\r\n" %
481                        (self.request_version, self.response_code,
482                         response_message))
483       self._SendHeader("Server", self.server_version)
484       self._SendHeader("Date", self._DateTimeHeader())
485       self._SendHeader("Content-Type", self.response_content_type)
486       self._SendHeader("Content-Length", str(len(self.response_body)))
487       # We don't support keep-alive at this time
488       self._SendHeader("Connection", "close")
489       self.wfile.write("\r\n")
490
491     if (self.request_method != HTTP_HEAD and
492         self.response_code >= HTTP_OK and
493         self.response_code not in (HTTP_NO_CONTENT, HTTP_NOT_MODIFIED)):
494       self.wfile.write(self.response_body)
495
496   def _SendHeader(self, name, value):
497     if self.request_version != HTTP_0_9:
498       self.wfile.write("%s: %s\r\n" % (name, value))
499
500   def _ReadRequest(self):
501     """Reads and parses request line
502
503     """
504     raw_requestline = self.rfile.readline()
505
506     requestline = raw_requestline
507     if requestline[-2:] == '\r\n':
508       requestline = requestline[:-2]
509     elif requestline[-1:] == '\n':
510       requestline = requestline[:-1]
511
512     if not requestline:
513       raise HTTPBadRequest("Empty request line")
514
515     self.request_requestline = requestline
516
517     logging.debug("HTTP request: %s", raw_requestline.rstrip("\r\n"))
518
519     words = requestline.split()
520
521     if len(words) == 3:
522       [method, path, version] = words
523       if version[:5] != 'HTTP/':
524         raise HTTPBadRequest("Bad request version (%r)" % version)
525
526       try:
527         base_version_number = version.split('/', 1)[1]
528         version_number = base_version_number.split(".")
529
530         # RFC 2145 section 3.1 says there can be only one "." and
531         #   - major and minor numbers MUST be treated as
532         #      separate integers;
533         #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
534         #      turn is lower than HTTP/12.3;
535         #   - Leading zeros MUST be ignored by recipients.
536         if len(version_number) != 2:
537           raise HTTPBadRequest("Bad request version (%r)" % version)
538
539         version_number = int(version_number[0]), int(version_number[1])
540       except (ValueError, IndexError):
541         raise HTTPBadRequest("Bad request version (%r)" % version)
542
543       if version_number >= (2, 0):
544         raise HTTPVersionNotSupported("Invalid HTTP Version (%s)" %
545                                       base_version_number)
546
547     elif len(words) == 2:
548       version = HTTP_0_9
549       [method, path] = words
550       if method != HTTP_GET:
551         raise HTTPBadRequest("Bad HTTP/0.9 request type (%r)" % method)
552
553     else:
554       raise HTTPBadRequest("Bad request syntax (%r)" % requestline)
555
556     # Examine the headers and look for a Connection directive
557     headers = mimetools.Message(self.rfile, 0)
558
559     self.request_method = method
560     self.request_path = path
561     self.request_version = version
562     self.request_headers = headers
563
564   def _ReadPostData(self):
565     """Reads POST/PUT data
566
567     """
568     if not self.request_method or self.request_method.upper() not in ("POST", "PUT"):
569       self.request_post_data = None
570       return
571
572     # TODO: Decide what to do when Content-Length header was not sent
573     try:
574       content_length = int(self.request_headers.get('Content-Length', 0))
575     except ValueError:
576       raise HTTPBadRequest("No Content-Length header or invalid format")
577
578     data = self.rfile.read(content_length)
579
580     # TODO: Content-type, error handling
581     self.request_post_data = HTTPJsonConverter().Decode(data)
582
583     logging.debug("HTTP POST data: %s", self.request_post_data)
584
585
586 class HttpServer(object):
587   """Generic HTTP server class
588
589   Users of this class must subclass it and override the HandleRequest function.
590   Optionally, the ForkForRequest function can be overriden.
591
592   """
593   MAX_CHILDREN = 20
594
595   def __init__(self, mainloop, server_address):
596     self.mainloop = mainloop
597     self.server_address = server_address
598
599     # TODO: SSL support
600     self.ssl_cert = None
601     self.ssl_key = self.ssl_cert
602
603     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
604
605     if self.ssl_cert and self.ssl_key:
606       ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
607       ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
608
609       ctx.use_certificate_file(self.ssl_cert)
610       ctx.use_privatekey_file(self.ssl_key)
611
612       self.socket = OpenSSL.SSL.Connection(ctx, sock)
613       self._fileio_class = _SSLFileObject
614     else:
615       self.socket = sock
616       self._fileio_class = socket._fileobject
617
618     # Allow port to be reused
619     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
620
621     self._children = []
622
623     mainloop.RegisterIO(self, self.socket.fileno(), select.POLLIN)
624     mainloop.RegisterSignal(self)
625
626   def Start(self):
627     self.socket.bind(self.server_address)
628     self.socket.listen(5)
629
630   def Stop(self):
631     self.socket.close()
632
633   def OnIO(self, fd, condition):
634     if condition & select.POLLIN:
635       self._IncomingConnection()
636
637   def OnSignal(self, signum):
638     if signum == signal.SIGCHLD:
639       self._CollectChildren(True)
640
641   def _CollectChildren(self, quick):
642     """Checks whether any child processes are done
643
644     @type quick: bool
645     @param quick: Whether to only use non-blocking functions
646
647     """
648     if not quick:
649       # Don't wait for other processes if it should be a quick check
650       while len(self._children) > self.MAX_CHILDREN:
651         try:
652           pid, status = os.waitpid(0, 0)
653         except os.error:
654           pid = None
655         if pid and pid in self._children:
656           self._children.remove(pid)
657
658     for child in self._children:
659       try:
660         pid, status = os.waitpid(child, os.WNOHANG)
661       except os.error:
662         pid = None
663       if pid and pid in self._children:
664         self._children.remove(pid)
665
666   def _IncomingConnection(self):
667     connection, client_addr = self.socket.accept()
668     logging.info("Connection from %s:%s", client_addr[0], client_addr[1])
669     try:
670       handler = _HttpConnectionHandler(self, connection, client_addr, self._fileio_class)
671     except (socket.error, SocketClosed):
672       return
673
674     def FinishRequest():
675       try:
676         try:
677           try:
678             handler.HandleRequest()
679           finally:
680             # Try to send a response
681             handler.SendResponse()
682             handler.Close()
683         except SocketClosed:
684           pass
685       finally:
686         logging.info("Disconnected %s:%s", client_addr[0], client_addr[1])
687
688     # Check whether we should fork or not
689     if not handler.should_fork:
690       FinishRequest()
691       return
692
693     self._CollectChildren(False)
694
695     pid = os.fork()
696     if pid == 0:
697       # Child process
698       try:
699         FinishRequest()
700       except:
701         logging.exception("Error while handling request from %s:%s",
702                           client_addr[0], client_addr[1])
703         os._exit(1)
704       os._exit(0)
705     else:
706       self._children.append(pid)
707
708   def HandleRequest(self, req):
709     raise NotImplementedError()
710
711   def ForkForRequest(self, req):
712     return True
713
714
715 class _SSLFileObject(object):
716   """Wrapper around socket._fileobject
717
718   This wrapper is required to handle OpenSSL exceptions.
719
720   """
721   def _RequireOpenSocket(fn):
722     def wrapper(self, *args, **kwargs):
723       if self.closed:
724         raise SocketClosed("Socket is closed")
725       return fn(self, *args, **kwargs)
726     return wrapper
727
728   def __init__(self, sock, mode='rb', bufsize=-1):
729     self._base = socket._fileobject(sock, mode=mode, bufsize=bufsize)
730
731   def _ConnectionLost(self):
732     self._base = None
733
734   def _getclosed(self):
735     return self._base is None or self._base.closed
736   closed = property(_getclosed, doc="True if the file is closed")
737
738   @_RequireOpenSocket
739   def close(self):
740     return self._base.close()
741
742   @_RequireOpenSocket
743   def flush(self):
744     return self._base.flush()
745
746   @_RequireOpenSocket
747   def fileno(self):
748     return self._base.fileno()
749
750   @_RequireOpenSocket
751   def read(self, size=-1):
752     return self._ReadWrapper(self._base.read, size=size)
753
754   @_RequireOpenSocket
755   def readline(self, size=-1):
756     return self._ReadWrapper(self._base.readline, size=size)
757
758   def _ReadWrapper(self, fn, *args, **kwargs):
759     while True:
760       try:
761         return fn(*args, **kwargs)
762
763       except OpenSSL.SSL.ZeroReturnError, err:
764         self._ConnectionLost()
765         return ""
766
767       except OpenSSL.SSL.WantReadError:
768         continue
769
770       #except OpenSSL.SSL.WantWriteError:
771       # TODO
772
773       except OpenSSL.SSL.SysCallError, (retval, desc):
774         if ((retval == -1 and desc == "Unexpected EOF")
775             or retval > 0):
776           self._ConnectionLost()
777           return ""
778
779         logging.exception("Error in OpenSSL")
780         self._ConnectionLost()
781         raise socket.error(err.args)
782
783       except OpenSSL.SSL.Error, err:
784         self._ConnectionLost()
785         raise socket.error(err.args)
786
787   @_RequireOpenSocket
788   def write(self, data):
789     return self._WriteWrapper(self._base.write, data)
790
791   def _WriteWrapper(self, fn, *args, **kwargs):
792     while True:
793       try:
794         return fn(*args, **kwargs)
795       except OpenSSL.SSL.ZeroReturnError, err:
796         self._ConnectionLost()
797         return 0
798
799       except OpenSSL.SSL.WantWriteError:
800         continue
801
802       #except OpenSSL.SSL.WantReadError:
803       # TODO
804
805       except OpenSSL.SSL.SysCallError, err:
806         if err.args[0] == -1 and data == "":
807           # errors when writing empty strings are expected
808           # and can be ignored
809           return 0
810
811         self._ConnectionLost()
812         raise socket.error(err.args)
813
814       except OpenSSL.SSL.Error, err:
815         self._ConnectionLost()
816         raise socket.error(err.args)