Slightly abstract the daemon logfile lookup
[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]",
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("--no-ssl", dest="ssl",
194                     help="Do not secure HTTP protocol with SSL",
195                     default=True, action="store_false")
196   parser.add_option("-K", "--ssl-key", dest="ssl_key",
197                     help="SSL key",
198                     default=constants.RAPI_CERT_FILE, type="string")
199   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
200                     help="SSL certificate",
201                     default=constants.RAPI_CERT_FILE, type="string")
202   parser.add_option("-f", "--foreground", dest="fork",
203                     help="Don't detach from the current terminal",
204                     default=True, action="store_false")
205   parser.add_option("-b", "--bind", dest="bind_address",
206                      help="Bind address",
207                      default="", metavar="ADDRESS")
208
209   options, args = parser.parse_args()
210
211   if len(args) != 0:
212     print >> sys.stderr, "Usage: %s [-d]" % sys.argv[0]
213     sys.exit(constants.EXIT_FAILURE)
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(constants.EXIT_FAILURE)
219
220   return options, args
221
222
223 def main():
224   """Main function.
225
226   """
227   options, args = ParseOptions()
228   daemon_name = constants.RAPI
229
230   if options.fork:
231     utils.CloseFDs()
232
233   if options.ssl:
234     # Read SSL certificate
235     try:
236       ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
237                                       ssl_cert_path=options.ssl_cert)
238     except Exception, err:
239       sys.stderr.write("Can't load the SSL certificate/key: %s\n" % (err,))
240       sys.exit(constants.EXIT_FAILURE)
241   else:
242     ssl_params = None
243
244   ssconf.CheckMaster(options.debug)
245   port = utils.GetDaemonPort(constants.RAPI)
246
247   if options.fork:
248     utils.Daemonize(logfile=constants.DAEMONS_LOGFILES[daemon_name])
249
250   utils.SetupLogging(constants.DAEMONS_LOGFILES[daemon_name], debug=options.debug,
251                      stderr_logging=not options.fork)
252
253   utils.WritePidFile(constants.RAPI_PID)
254   try:
255     mainloop = daemon.Mainloop()
256     server = RemoteApiHttpServer(mainloop, options.bind_address, port,
257                                  ssl_params=ssl_params, ssl_verify_peer=False,
258                                  request_executor_class=
259                                  JsonErrorRequestExecutor)
260     server.Start()
261     try:
262       mainloop.Run()
263     finally:
264       server.Stop()
265   finally:
266     utils.RemovePidFile(constants.RAPI_PID)
267
268
269 if __name__ == '__main__':
270   main()