rapi: fix authentication and queries
[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.rapi import connector
39
40 import ganeti.http.auth
41 import ganeti.http.server
42
43
44 class RemoteApiRequestContext(object):
45   """Data structure for Remote API requests.
46
47   """
48   def __init__(self):
49     self.handler = None
50     self.handler_fn = None
51     self.handler_access = None
52
53
54 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
55                           http.server.HttpServer):
56   """REST Request Handler Class.
57
58   """
59   AUTH_REALM = "Ganeti Remote API"
60
61   def __init__(self, *args, **kwargs):
62     http.server.HttpServer.__init__(self, *args, **kwargs)
63     http.auth.HttpServerRequestAuthentication.__init__(self)
64     self._resmap = connector.Mapper()
65
66     # Load password file
67     if os.path.isfile(constants.RAPI_USERS_FILE):
68       self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
69     else:
70       self._users = None
71
72   def _GetRequestContext(self, req):
73     """Returns the context for a request.
74
75     The context is cached in the req.private variable.
76
77     """
78     if req.private is None:
79       (HandlerClass, items, args) = \
80                      self._resmap.getController(req.request_path)
81
82       ctx = RemoteApiRequestContext()
83       ctx.handler = HandlerClass(items, args, req)
84
85       method = req.request_method.upper()
86       try:
87         ctx.handler_fn = getattr(ctx.handler, method)
88       except AttributeError, err:
89         raise http.HttpBadRequest()
90
91       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
92
93       # Require permissions definition (usually in the base class)
94       if ctx.handler_access is None:
95         raise AssertionError("Permissions definition missing")
96
97       req.private = ctx
98
99     return req.private
100
101   def GetAuthRealm(self, req):
102     """Override the auth realm for queries.
103
104     """
105     ctx = self._GetRequestContext(req)
106     if ctx.handler_access:
107       return self.AUTH_REALM
108     else:
109       return None
110
111   def Authenticate(self, req, username, password):
112     """Checks whether a user can access a resource.
113
114     """
115     ctx = self._GetRequestContext(req)
116
117     # Check username and password
118     valid_user = False
119     if self._users:
120       user = self._users.get(username, None)
121       if user and user.password == password:
122         valid_user = True
123
124     if not valid_user:
125       # Unknown user or password wrong
126       return False
127
128     if (not ctx.handler_access or
129         set(user.options).intersection(ctx.handler_access)):
130       # Allow access
131       return True
132
133     # Access forbidden
134     raise http.HttpForbidden()
135
136   def HandleRequest(self, req):
137     """Handles a request.
138
139     """
140     ctx = self._GetRequestContext(req)
141
142     try:
143       result = ctx.handler_fn()
144       sn = ctx.handler.getSerialNumber()
145       if sn:
146         req.response_headers[http.HTTP_ETAG] = str(sn)
147     except:
148       method = req.request_method.upper()
149       logging.exception("Error while handling the %s request", method)
150       raise
151
152     return result
153
154
155 def ParseOptions():
156   """Parse the command line options.
157
158   @return: (options, args) as from OptionParser.parse_args()
159
160   """
161   parser = optparse.OptionParser(description="Ganeti Remote API",
162                     usage="%prog [-d] [-p port]",
163                     version="%%prog (ganeti) %s" %
164                                  constants.RAPI_VERSION)
165   parser.add_option("-d", "--debug", dest="debug",
166                     help="Enable some debug messages",
167                     default=False, action="store_true")
168   parser.add_option("-p", "--port", dest="port",
169                     help="Port to run API (%s default)." %
170                                  constants.RAPI_PORT,
171                     default=constants.RAPI_PORT, type="int")
172   parser.add_option("-S", "--https", dest="ssl",
173                     help="Secure HTTP protocol with SSL",
174                     default=False, action="store_true")
175   parser.add_option("-K", "--ssl-key", dest="ssl_key",
176                     help="SSL key",
177                     default=None, type="string")
178   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
179                     help="SSL certificate",
180                     default=None, type="string")
181   parser.add_option("-f", "--foreground", dest="fork",
182                     help="Don't detach from the current terminal",
183                     default=True, action="store_false")
184
185   options, args = parser.parse_args()
186
187   if len(args) != 0:
188     print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
189     sys.exit(1)
190
191   if options.ssl and not (options.ssl_cert and options.ssl_key):
192     print >> sys.stderr, ("For secure mode please provide "
193                          "--ssl-key and --ssl-cert arguments")
194     sys.exit(1)
195
196   return options, args
197
198
199 def main():
200   """Main function.
201
202   """
203   options, args = ParseOptions()
204
205   if options.fork:
206     utils.CloseFDs()
207
208   ssconf.CheckMaster(options.debug)
209
210   if options.fork:
211     utils.Daemonize(logfile=constants.LOG_RAPISERVER)
212
213   utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
214                      stderr_logging=not options.fork)
215
216   utils.WritePidFile(constants.RAPI_PID)
217   try:
218     mainloop = daemon.Mainloop()
219     server = RemoteApiHttpServer(mainloop, "", options.port)
220     server.Start()
221     try:
222       mainloop.Run()
223     finally:
224       server.Stop()
225   finally:
226     utils.RemovePidFile(constants.RAPI_PID)
227
228
229 if __name__ == '__main__':
230   main()