ganeti-rapi: Implement HTTP authentication
[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) = self._resmap.getController(req.request_path)
80
81       ctx = RemoteApiRequestContext()
82       ctx.handler = HandlerClass(items, args, req)
83
84       method = req.request_method.upper()
85       try:
86         ctx.handler_fn = getattr(ctx.handler, method)
87       except AttributeError, err:
88         raise http.HttpBadRequest()
89
90       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
91
92       # Require permissions definition (usually in the base class)
93       if ctx.handler_access is None:
94         raise AssertionError("Permissions definition missing")
95
96       req.private = ctx
97
98     return req.private
99
100   def Authenticate(self, req, username, password):
101     """Checks whether a user can access a resource.
102
103     """
104     ctx = self._GetRequestContext(req)
105
106     # Check username and password
107     valid_user = False
108     if self._users:
109       user = self._users.get(username, None)
110       if user and user.password == password:
111         valid_user = True
112
113     if not valid_user:
114       # Unknown user or password wrong
115       return False
116
117     if (not ctx.handler_access or
118         set(user.options).intersection(ctx.handler_access)):
119       # Allow access
120       return True
121
122     # Access forbidden
123     raise http.HttpForbidden()
124
125   def HandleRequest(self, req):
126     """Handles a request.
127
128     """
129     ctx = self._GetRequestContext(req)
130
131     try:
132       result = ctx.handler_fn()
133       sn = ctx.handler.getSerialNumber()
134       if sn:
135         req.response_headers[http.HTTP_ETAG] = str(sn)
136     except:
137       logging.exception("Error while handling the %s request", method)
138       raise
139
140     return result
141
142
143 def ParseOptions():
144   """Parse the command line options.
145
146   @return: (options, args) as from OptionParser.parse_args()
147
148   """
149   parser = optparse.OptionParser(description="Ganeti Remote API",
150                     usage="%prog [-d] [-p port]",
151                     version="%%prog (ganeti) %s" %
152                                  constants.RAPI_VERSION)
153   parser.add_option("-d", "--debug", dest="debug",
154                     help="Enable some debug messages",
155                     default=False, action="store_true")
156   parser.add_option("-p", "--port", dest="port",
157                     help="Port to run API (%s default)." %
158                                  constants.RAPI_PORT,
159                     default=constants.RAPI_PORT, type="int")
160   parser.add_option("-S", "--https", dest="ssl",
161                     help="Secure HTTP protocol with SSL",
162                     default=False, action="store_true")
163   parser.add_option("-K", "--ssl-key", dest="ssl_key",
164                     help="SSL key",
165                     default=None, type="string")
166   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
167                     help="SSL certificate",
168                     default=None, type="string")
169   parser.add_option("-f", "--foreground", dest="fork",
170                     help="Don't detach from the current terminal",
171                     default=True, action="store_false")
172
173   options, args = parser.parse_args()
174
175   if len(args) != 0:
176     print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
177     sys.exit(1)
178
179   if options.ssl and not (options.ssl_cert and options.ssl_key):
180     print >> sys.stderr, ("For secure mode please provide "
181                          "--ssl-key and --ssl-cert arguments")
182     sys.exit(1)
183
184   return options, args
185
186
187 def main():
188   """Main function.
189
190   """
191   options, args = ParseOptions()
192
193   ssconf.CheckMaster(options.debug)
194
195   if options.fork:
196     utils.Daemonize(logfile=constants.LOG_RAPISERVER)
197
198   utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
199                      stderr_logging=not options.fork)
200
201   utils.WritePidFile(constants.RAPI_PID)
202   try:
203     mainloop = daemon.Mainloop()
204     server = RemoteApiHttpServer(mainloop, "", options.port)
205     server.Start()
206     try:
207       mainloop.Run()
208     finally:
209       server.Stop()
210   finally:
211     utils.RemovePidFile(constants.RAPI_PID)
212
213
214 if __name__ == '__main__':
215   main()