Split RAPI resources to pieces
[ganeti-local] / lib / rapi / RESTHTTPServer.py
1 #
2 #
3
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 # 02110-1301, USA.
18
19 """RESTfull HTTPS Server module.
20
21 """
22
23 import BaseHTTPServer
24 import OpenSSL
25 import re
26 import socket
27 import time
28
29 from ganeti import constants
30 from ganeti import errors
31 from ganeti import logger
32 from ganeti import rpc
33 from ganeti import serializer
34
35 from ganeti.rapi import connector
36 from ganeti.rapi import httperror
37
38
39 class HttpLogfile:
40   """Utility class to write HTTP server log files.
41
42   The written format is the "Common Log Format" as defined by Apache:
43   http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
44
45   """
46   MONTHNAME = [None,
47                'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
48                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
49
50   def __init__(self, path):
51     self._fd = open(path, 'a', 1)
52
53   def __del__(self):
54     try:
55       self.Close()
56     except:
57       # Swallow exceptions
58       pass
59
60   def Close(self):
61     if self._fd is not None:
62       self._fd.close()
63       self._fd = None
64
65   def LogRequest(self, request, format, *args):
66     if self._fd is None:
67       raise errors.ProgrammerError("Logfile already closed")
68
69     request_time = self._FormatCurrentTime()
70
71     self._fd.write("%s %s %s [%s] %s\n" % (
72       # Remote host address
73       request.address_string(),
74
75       # RFC1413 identity (identd)
76       "-",
77
78       # Remote user
79       "-",
80
81       # Request time
82       request_time,
83
84       # Message
85       format % args,
86       ))
87
88   def _FormatCurrentTime(self):
89     """Formats current time in Common Log Format.
90
91     """
92     return self._FormatLogTime(time.time())
93
94   def _FormatLogTime(self, seconds):
95     """Formats time for Common Log Format.
96
97     All timestamps are logged in the UTC timezone.
98
99     Args:
100     - seconds: Time in seconds since the epoch
101
102     """
103     (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
104     format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
105     return time.strftime(format, tm)
106
107
108 class RESTHTTPServer(BaseHTTPServer.HTTPServer):
109   """Class to provide an HTTP/HTTPS server.
110
111   """
112   allow_reuse_address = True
113
114   def __init__(self, server_address, HandlerClass, options):
115     """REST Server Constructor.
116
117     Args:
118       server_address: a touple containing:
119         ip: a string with IP address, localhost if empty string
120         port: port number, integer
121       HandlerClass: HTTPRequestHandler object
122       options: Command-line options
123
124     """
125     logger.SetupLogging(debug=options.debug, program='ganeti-rapi')
126
127     self.httplog = HttpLogfile(constants.LOG_RAPIACCESS)
128
129     BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
130     if options.ssl:
131       # Set up SSL
132       context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
133       context.use_privatekey_file(options.ssl_key)
134       context.use_certificate_file(options.ssl_cert)
135       self.socket = OpenSSL.SSL.Connection(context,
136                                            socket.socket(self.address_family,
137                                            self.socket_type))
138     else:
139       self.socket = socket.socket(self.address_family, self.socket_type)
140
141     self.server_bind()
142     self.server_activate()
143
144
145 class JsonResponse:
146   CONTENT_TYPE = "application/json"
147
148   def Encode(self, data):
149     return serializer.DumpJson(data)
150
151
152 class RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
153   """REST Request Handler Class.
154
155   """
156   def setup(self):
157     """Setup secure read and write file objects.
158
159     """
160     self.connection = self.request
161     self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
162     self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
163     self._resmap = connector.Mapper()
164
165   def handle_one_request(self):
166     """Handle a single REST request.
167
168     """
169     self.raw_requestline = None
170     try:
171       self.raw_requestline = self.rfile.readline()
172     except OpenSSL.SSL.Error, ex:
173       logger.Error("Error in SSL: %s" % str(ex))
174     if not self.raw_requestline:
175       self.close_connection = 1
176       return
177     if not self.parse_request(): # An error code has been sent, just exit
178       return
179
180     try:
181       (HandlerClass, items, args) = self._resmap.getController(self.path)
182       handler = HandlerClass(self, items, args)
183
184       command = self.command.upper()
185       try:
186         fn = getattr(handler, command)
187       except AttributeError, err:
188         raise httperror.HTTPBadRequest()
189
190       try:
191         result = fn()
192
193       except errors.OpPrereqError, err:
194         # TODO: "Not found" is not always the correct error. Ganeti's core must
195         # differentiate between different error types.
196         raise httperror.HTTPNotFound(message=str(err))
197
198       encoder = JsonResponse()
199       encoded_result = encoder.Encode(result)
200
201       self.send_response(200)
202       self.send_header("Content-Type", encoder.CONTENT_TYPE)
203       self.end_headers()
204       self.wfile.write(encoded_result)
205
206     except httperror.HTTPException, err:
207       self.send_error(err.code, message=err.message)
208
209     except Exception, err:
210       self.send_error(httperror.HTTPInternalError.code, message=str(err))
211
212   def log_message(self, format, *args):
213     """Log an arbitrary message.
214
215     This is used by all other logging functions.
216
217     The first argument, FORMAT, is a format string for the
218     message to be logged.  If the format string contains
219     any % escapes requiring parameters, they should be
220     specified as subsequent arguments (it's just like
221     printf!).
222
223     """
224     self.server.httplog.LogRequest(self, format, *args)
225
226
227 def start(options):
228   # Disable signal handlers, otherwise we can't exit the daemon in a clean way
229   # by sending a signal.
230   rpc.install_twisted_signal_handlers = False
231
232   httpd = RESTHTTPServer(("", options.port), RESTRequestHandler, options)
233   try:
234     httpd.serve_forever()
235   finally:
236     httpd.server_close()