Remove references to utils.debug
[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("Method %s is unsupported for path %s" %
110                                   (method, req.request_path))
111
112       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
113
114       # Require permissions definition (usually in the base class)
115       if ctx.handler_access is None:
116         raise AssertionError("Permissions definition missing")
117
118       req.private = ctx
119
120     return req.private
121
122   def GetAuthRealm(self, req):
123     """Override the auth realm for queries.
124
125     """
126     ctx = self._GetRequestContext(req)
127     if ctx.handler_access:
128       return self.AUTH_REALM
129     else:
130       return None
131
132   def Authenticate(self, req, username, password):
133     """Checks whether a user can access a resource.
134
135     """
136     ctx = self._GetRequestContext(req)
137
138     # Check username and password
139     valid_user = False
140     if self._users:
141       user = self._users.get(username, None)
142       if user and user.password == password:
143         valid_user = True
144
145     if not valid_user:
146       # Unknown user or password wrong
147       return False
148
149     if (not ctx.handler_access or
150         set(user.options).intersection(ctx.handler_access)):
151       # Allow access
152       return True
153
154     # Access forbidden
155     raise http.HttpForbidden()
156
157   def HandleRequest(self, req):
158     """Handles a request.
159
160     """
161     ctx = self._GetRequestContext(req)
162
163     try:
164       result = ctx.handler_fn()
165       sn = ctx.handler.getSerialNumber()
166       if sn:
167         req.response_headers[http.HTTP_ETAG] = str(sn)
168     except luxi.TimeoutError:
169       raise http.HttpGatewayTimeout()
170     except luxi.ProtocolError, err:
171       raise http.HttpBadGateway(str(err))
172     except:
173       method = req.request_method.upper()
174       logging.exception("Error while handling the %s request", method)
175       raise
176
177     return result
178
179
180 def ParseOptions():
181   """Parse the command line options.
182
183   @return: (options, args) as from OptionParser.parse_args()
184
185   """
186   parser = optparse.OptionParser(description="Ganeti Remote API",
187                     usage="%prog [-d] [-p port]",
188                     version="%%prog (ganeti) %s" %
189                                  constants.RAPI_VERSION)
190   parser.add_option("-d", "--debug", dest="debug",
191                     help="Enable some debug messages",
192                     default=False, action="store_true")
193   parser.add_option("-p", "--port", dest="port",
194                     help="Port to run API (%s default)." %
195                                  constants.RAPI_PORT,
196                     default=constants.RAPI_PORT, type="int")
197   parser.add_option("--no-ssl", dest="ssl",
198                     help="Do not secure HTTP protocol with SSL",
199                     default=True, action="store_false")
200   parser.add_option("-K", "--ssl-key", dest="ssl_key",
201                     help="SSL key",
202                     default=constants.RAPI_CERT_FILE, type="string")
203   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
204                     help="SSL certificate",
205                     default=constants.RAPI_CERT_FILE, type="string")
206   parser.add_option("-f", "--foreground", dest="fork",
207                     help="Don't detach from the current terminal",
208                     default=True, action="store_false")
209   parser.add_option("-b", "--bind", dest="bind_address",
210                      help="Bind address",
211                      default="", metavar="ADDRESS")
212
213   options, args = parser.parse_args()
214
215   if len(args) != 0:
216     print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
217     sys.exit(constants.EXIT_FAILURE)
218
219   if options.ssl and not (options.ssl_cert and options.ssl_key):
220     print >> sys.stderr, ("For secure mode please provide "
221                           "--ssl-key and --ssl-cert arguments")
222     sys.exit(constants.EXIT_FAILURE)
223
224   return options, args
225
226
227 def main():
228   """Main function.
229
230   """
231   options, args = ParseOptions()
232
233   if options.fork:
234     utils.CloseFDs()
235
236   if options.ssl:
237     # Read SSL certificate
238     try:
239       ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
240                                       ssl_cert_path=options.ssl_cert)
241     except Exception, err:
242       sys.stderr.write("Can't load the SSL certificate/key: %s\n" % (err,))
243       sys.exit(constants.EXIT_FAILURE)
244   else:
245     ssl_params = None
246
247   ssconf.CheckMaster(options.debug)
248
249   if options.fork:
250     utils.Daemonize(logfile=constants.LOG_RAPISERVER)
251
252   utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
253                      stderr_logging=not options.fork)
254
255   utils.WritePidFile(constants.RAPI_PID)
256   try:
257     mainloop = daemon.Mainloop()
258     server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
259                                  ssl_params=ssl_params, ssl_verify_peer=False,
260                                  request_executor_class=
261                                  JsonErrorRequestExecutor)
262     server.Start()
263     try:
264       mainloop.Run()
265     finally:
266       server.Stop()
267   finally:
268     utils.RemovePidFile(constants.RAPI_PID)
269
270
271 if __name__ == '__main__':
272   main()