Change JobStorage to work with ids not filenames
[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 socket
23 import BaseHTTPServer
24 import OpenSSL
25 import time
26 import logging
27
28 from ganeti import errors
29 from ganeti import logger
30 from ganeti import serializer
31
32
33 class HTTPException(Exception):
34   code = None
35   message = None
36
37   def __init__(self, message=None):
38     if message is not None:
39       self.message = message
40
41
42 class HTTPBadRequest(HTTPException):
43   code = 400
44
45
46 class HTTPForbidden(HTTPException):
47   code = 403
48
49
50 class HTTPNotFound(HTTPException):
51   code = 404
52
53
54 class HTTPGone(HTTPException):
55   code = 410
56
57
58 class HTTPLengthRequired(HTTPException):
59   code = 411
60
61
62 class HTTPInternalError(HTTPException):
63   code = 500
64
65
66 class HTTPNotImplemented(HTTPException):
67   code = 501
68
69
70 class HTTPServiceUnavailable(HTTPException):
71   code = 503
72
73
74 class ApacheLogfile:
75   """Utility class to write HTTP server log files.
76
77   The written format is the "Common Log Format" as defined by Apache:
78   http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
79
80   """
81   MONTHNAME = [None,
82                'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
83                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
84
85   def __init__(self, fd):
86     """Constructor for ApacheLogfile class.
87
88     Args:
89     - fd: Open file object
90
91     """
92     self._fd = fd
93
94   def LogRequest(self, request, format, *args):
95     self._fd.write("%s %s %s [%s] %s\n" % (
96       # Remote host address
97       request.address_string(),
98
99       # RFC1413 identity (identd)
100       "-",
101
102       # Remote user
103       "-",
104
105       # Request time
106       self._FormatCurrentTime(),
107
108       # Message
109       format % args,
110       ))
111
112   def _FormatCurrentTime(self):
113     """Formats current time in Common Log Format.
114
115     """
116     return self._FormatLogTime(time.time())
117
118   def _FormatLogTime(self, seconds):
119     """Formats time for Common Log Format.
120
121     All timestamps are logged in the UTC timezone.
122
123     Args:
124     - seconds: Time in seconds since the epoch
125
126     """
127     (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
128     format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
129     return time.strftime(format, tm)
130
131
132 class HTTPServer(BaseHTTPServer.HTTPServer, object):
133   """Class to provide an HTTP/HTTPS server.
134
135   """
136   allow_reuse_address = True
137
138   def __init__(self, server_address, HandlerClass, httplog=None,
139                enable_ssl=False, ssl_key=None, ssl_cert=None):
140     """Server constructor.
141
142     Args:
143       server_address: a touple containing:
144         ip: a string with IP address, localhost if empty string
145         port: port number, integer
146       HandlerClass: HTTPRequestHandler object
147       httplog: Access log object
148       enable_ssl: Whether to enable SSL
149       ssl_key: SSL key file
150       ssl_cert: SSL certificate key
151
152     """
153     BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
154
155     self.httplog = httplog
156
157     if enable_ssl:
158       # Set up SSL
159       context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
160       context.use_privatekey_file(ssl_key)
161       context.use_certificate_file(ssl_cert)
162       self.socket = OpenSSL.SSL.Connection(context,
163                                            socket.socket(self.address_family,
164                                            self.socket_type))
165     else:
166       self.socket = socket.socket(self.address_family, self.socket_type)
167
168     self.server_bind()
169     self.server_activate()
170
171
172 class HTTPJsonConverter:
173   CONTENT_TYPE = "application/json"
174
175   def Encode(self, data):
176     return serializer.DumpJson(data)
177
178   def Decode(self, data):
179     return serializer.LoadJson(data)
180
181
182 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
183   """Request handler class.
184
185   """
186   def setup(self):
187     """Setup secure read and write file objects.
188
189     """
190     self.connection = self.request
191     self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
192     self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
193
194   def handle_one_request(self):
195     """Parses a request and calls the handler function.
196
197     """
198     self.raw_requestline = None
199     try:
200       self.raw_requestline = self.rfile.readline()
201     except OpenSSL.SSL.Error, ex:
202       logger.Error("Error in SSL: %s" % str(ex))
203     if not self.raw_requestline:
204       self.close_connection = 1
205       return
206     if not self.parse_request(): # An error code has been sent, just exit
207       return
208     logging.debug("HTTP request: %s", self.raw_requestline.rstrip("\r\n"))
209
210     try:
211       self._ReadPostData()
212
213       result = self.HandleRequest()
214
215       # TODO: Content-type
216       encoder = HTTPJsonConverter()
217       encoded_result = encoder.Encode(result)
218
219       self.send_response(200)
220       self.send_header("Content-Type", encoder.CONTENT_TYPE)
221       self.send_header("Content-Length", str(len(encoded_result)))
222       self.end_headers()
223
224       self.wfile.write(encoded_result)
225
226     except HTTPException, err:
227       self.send_error(err.code, message=err.message)
228
229     except Exception, err:
230       self.send_error(HTTPInternalError.code, message=str(err))
231
232     except:
233       self.send_error(HTTPInternalError.code, message="Unknown error")
234
235   def _ReadPostData(self):
236     if self.command.upper() not in ("POST", "PUT"):
237       self.post_data = None
238       return
239
240     # TODO: Decide what to do when Content-Length header was not sent
241     try:
242       content_length = int(self.headers.get('Content-Length', 0))
243     except ValueError:
244       raise HTTPBadRequest("No Content-Length header or invalid format")
245
246     try:
247       data = self.rfile.read(content_length)
248     except socket.error, err:
249       logger.Error("Socket error while reading: %s" % str(err))
250       return
251
252     # TODO: Content-type, error handling
253     self.post_data = HTTPJsonConverter().Decode(data)
254
255     logging.debug("HTTP POST data: %s", self.post_data)
256
257   def HandleRequest(self):
258     """Handles a request.
259
260     """
261     raise NotImplementedError()
262
263   def log_message(self, format, *args):
264     """Log an arbitrary message.
265
266     This is used by all other logging functions.
267
268     The first argument, FORMAT, is a format string for the
269     message to be logged.  If the format string contains
270     any % escapes requiring parameters, they should be
271     specified as subsequent arguments (it's just like
272     printf!).
273
274     """
275     logging.debug("Handled request: %s", format % args)
276     if self.server.httplog:
277       self.server.httplog.LogRequest(self, format, *args)