RAPI: format error messages as JSON
[ganeti-local] / daemons / ganeti-rapi
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """ Ganeti Remote API master script.
22 """
23
24 import glob
25 import logging
26 import optparse
27 import sys
28 import os
29 import os.path
30 import signal
31
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import http
35 from ganeti import daemon
36 from ganeti import ssconf
37 from ganeti import utils
38 from ganeti import luxi
39 from ganeti import serializer
40 from ganeti.rapi import connector
41
42 import ganeti.http.auth
43 import ganeti.http.server
44
45
46 class RemoteApiRequestContext(object):
47   """Data structure for Remote API requests.
48
49   """
50   def __init__(self):
51     self.handler = None
52     self.handler_fn = None
53     self.handler_access = None
54
55
56 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
57   """Custom Request Executor class that formats HTTP errors in JSON.
58
59   """
60   error_content_type = "application/json"
61
62   def _FormatErrorMessage(self, values):
63     """Formats the body of an error message.
64
65     @type values: dict
66     @param values: dictionary with keys code, message and explain.
67     @rtype: string
68     @return: the body of the message
69
70     """
71     return serializer.DumpJson(values, indent=True)
72
73
74 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
75                           http.server.HttpServer):
76   """REST Request Handler Class.
77
78   """
79   AUTH_REALM = "Ganeti Remote API"
80
81   def __init__(self, *args, **kwargs):
82     http.server.HttpServer.__init__(self, *args, **kwargs)
83     http.auth.HttpServerRequestAuthentication.__init__(self)
84     self._resmap = connector.Mapper()
85
86     # Load password file
87     if os.path.isfile(constants.RAPI_USERS_FILE):
88       self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
89     else:
90       self._users = None
91
92   def _GetRequestContext(self, req):
93     """Returns the context for a request.
94
95     The context is cached in the req.private variable.
96
97     """
98     if req.private is None:
99       (HandlerClass, items, args) = \
100                      self._resmap.getController(req.request_path)
101
102       ctx = RemoteApiRequestContext()
103       ctx.handler = HandlerClass(items, args, req)
104
105       method = req.request_method.upper()
106       try:
107         ctx.handler_fn = getattr(ctx.handler, method)
108       except AttributeError, err:
109         raise http.HttpBadRequest()
110
111       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
112
113       # Require permissions definition (usually in the base class)
114       if ctx.handler_access is None:
115         raise AssertionError("Permissions definition missing")
116
117       req.private = ctx
118
119     return req.private
120
121   def GetAuthRealm(self, req):
122     """Override the auth realm for queries.
123
124     """
125     ctx = self._GetRequestContext(req)
126     if ctx.handler_access:
127       return self.AUTH_REALM
128     else:
129       return None
130
131   def Authenticate(self, req, username, password):
132     """Checks whether a user can access a resource.
133
134     """
135     ctx = self._GetRequestContext(req)
136
137     # Check username and password
138     valid_user = False
139     if self._users:
140       user = self._users.get(username, None)
141       if user and user.password == password:
142         valid_user = True
143
144     if not valid_user:
145       # Unknown user or password wrong
146       return False
147
148     if (not ctx.handler_access or
149         set(user.options).intersection(ctx.handler_access)):
150       # Allow access
151       return True
152
153     # Access forbidden
154     raise http.HttpForbidden()
155
156   def HandleRequest(self, req):
157     """Handles a request.
158
159     """
160     ctx = self._GetRequestContext(req)
161
162     try:
163       result = ctx.handler_fn()
164       sn = ctx.handler.getSerialNumber()
165       if sn:
166         req.response_headers[http.HTTP_ETAG] = str(sn)
167     except luxi.TimeoutError:
168       raise http.HttpGatewayTimeout()
169     except luxi.ProtocolError, err:
170       raise http.HttpBadGateway(str(err))
171     except:
172       method = req.request_method.upper()
173       logging.exception("Error while handling the %s request", method)
174       raise
175
176     return result
177
178
179 def ParseOptions():
180   """Parse the command line options.
181
182   @return: (options, args) as from OptionParser.parse_args()
183
184   """
185   parser = optparse.OptionParser(description="Ganeti Remote API",
186                     usage="%prog [-d] [-p port]",
187                     version="%%prog (ganeti) %s" %
188                                  constants.RAPI_VERSION)
189   parser.add_option("-d", "--debug", dest="debug",
190                     help="Enable some debug messages",
191                     default=False, action="store_true")
192   parser.add_option("-p", "--port", dest="port",
193                     help="Port to run API (%s default)." %
194                                  constants.RAPI_PORT,
195                     default=constants.RAPI_PORT, type="int")
196   parser.add_option("--no-ssl", dest="ssl",
197                     help="Do not secure HTTP protocol with SSL",
198                     default=True, action="store_false")
199   parser.add_option("-K", "--ssl-key", dest="ssl_key",
200                     help="SSL key",
201                     default=constants.RAPI_CERT_FILE, type="string")
202   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
203                     help="SSL certificate",
204                     default=constants.RAPI_CERT_FILE, type="string")
205   parser.add_option("-f", "--foreground", dest="fork",
206                     help="Don't detach from the current terminal",
207                     default=True, action="store_false")
208
209   options, args = parser.parse_args()
210
211   if len(args) != 0:
212     print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
213     sys.exit(1)
214
215   if options.ssl and not (options.ssl_cert and options.ssl_key):
216     print >> sys.stderr, ("For secure mode please provide "
217                          "--ssl-key and --ssl-cert arguments")
218     sys.exit(1)
219
220   return options, args
221
222
223 def main():
224   """Main function.
225
226   """
227   options, args = ParseOptions()
228
229   if options.fork:
230     utils.CloseFDs()
231
232   if options.ssl:
233     # Read SSL certificate
234     try:
235       ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
236                                       ssl_cert_path=options.ssl_cert)
237     except Exception, err:
238       sys.stderr.write("Can't load the SSL certificate/key: %s\n" % (err,))
239       sys.exit(1)
240   else:
241     ssl_params = None
242
243   ssconf.CheckMaster(options.debug)
244
245   if options.fork:
246     utils.Daemonize(logfile=constants.LOG_RAPISERVER)
247
248   utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
249                      stderr_logging=not options.fork)
250
251   utils.WritePidFile(constants.RAPI_PID)
252   try:
253     mainloop = daemon.Mainloop()
254     server = RemoteApiHttpServer(mainloop, "", options.port,
255                                  ssl_params=ssl_params, ssl_verify_peer=False,
256                                  request_executor_class=
257                                  JsonErrorRequestExecutor)
258     server.Start()
259     try:
260       mainloop.Run()
261     finally:
262       server.Stop()
263   finally:
264     utils.RemovePidFile(constants.RAPI_PID)
265
266
267 if __name__ == '__main__':
268   main()