Modify gnt-node add to call external script
[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
25 # pylint: disable-msg=C0103,W0142
26
27 # C0103: Invalid name ganeti-watcher
28
29 import logging
30 import optparse
31 import sys
32 import os
33 import os.path
34
35 from ganeti import constants
36 from ganeti import http
37 from ganeti import daemon
38 from ganeti import ssconf
39 from ganeti import luxi
40 from ganeti import serializer
41 from ganeti.rapi import connector
42
43 import ganeti.http.auth   # pylint: disable-msg=W0611
44 import ganeti.http.server
45
46
47 class RemoteApiRequestContext(object):
48   """Data structure for Remote API requests.
49
50   """
51   def __init__(self):
52     self.handler = None
53     self.handler_fn = None
54     self.handler_access = None
55     self.body_data = None
56
57
58 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
59   """Custom Request Executor class that formats HTTP errors in JSON.
60
61   """
62   error_content_type = http.HTTP_APP_JSON
63
64   def _FormatErrorMessage(self, values):
65     """Formats the body of an error message.
66
67     @type values: dict
68     @param values: dictionary with keys code, message and explain.
69     @rtype: string
70     @return: the body of the message
71
72     """
73     return serializer.DumpJson(values, indent=True)
74
75
76 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
77                           http.server.HttpServer):
78   """REST Request Handler Class.
79
80   """
81   AUTH_REALM = "Ganeti Remote API"
82
83   def __init__(self, *args, **kwargs):
84     # pylint: disable-msg=W0233
85   # it seems pylint doesn't see the second parent class there
86     http.server.HttpServer.__init__(self, *args, **kwargs)
87     http.auth.HttpServerRequestAuthentication.__init__(self)
88     self._resmap = connector.Mapper()
89
90     # Load password file
91     if os.path.isfile(constants.RAPI_USERS_FILE):
92       self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
93     else:
94       self._users = None
95
96   def _GetRequestContext(self, req):
97     """Returns the context for a request.
98
99     The context is cached in the req.private variable.
100
101     """
102     if req.private is None:
103       (HandlerClass, items, args) = \
104                      self._resmap.getController(req.request_path)
105
106       ctx = RemoteApiRequestContext()
107       ctx.handler = HandlerClass(items, args, req)
108
109       method = req.request_method.upper()
110       try:
111         ctx.handler_fn = getattr(ctx.handler, method)
112       except AttributeError:
113         raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
114                                       (method, req.request_path))
115
116       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
117
118       # Require permissions definition (usually in the base class)
119       if ctx.handler_access is None:
120         raise AssertionError("Permissions definition missing")
121
122       # This is only made available in HandleRequest
123       ctx.body_data = None
124
125       req.private = ctx
126
127     # Check for expected attributes
128     assert req.private.handler
129     assert req.private.handler_fn
130     assert req.private.handler_access is not None
131
132     return req.private
133
134   def AuthenticationRequired(self, req):
135     """Determine whether authentication is required.
136
137     """
138     return bool(self._GetRequestContext(req).handler_access)
139
140   def Authenticate(self, req, username, password):
141     """Checks whether a user can access a resource.
142
143     """
144     ctx = self._GetRequestContext(req)
145
146     # Check username and password
147     valid_user = False
148     if self._users:
149       user = self._users.get(username, None)
150       if user and self.VerifyBasicAuthPassword(req, username, password,
151                                                user.password):
152         valid_user = True
153
154     if not valid_user:
155       # Unknown user or password wrong
156       return False
157
158     if (not ctx.handler_access or
159         set(user.options).intersection(ctx.handler_access)):
160       # Allow access
161       return True
162
163     # Access forbidden
164     raise http.HttpForbidden()
165
166   def HandleRequest(self, req):
167     """Handles a request.
168
169     """
170     ctx = self._GetRequestContext(req)
171
172     # Deserialize request parameters
173     if req.request_body:
174       # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
175       # include a Content-Type header field defining the media type of that
176       # body. [...] If the media type remains unknown, the recipient SHOULD
177       # treat it as type "application/octet-stream".
178       req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
179                                                  http.HTTP_APP_OCTET_STREAM)
180       if req_content_type.lower() != http.HTTP_APP_JSON.lower():
181         raise http.HttpUnsupportedMediaType()
182
183       try:
184         ctx.body_data = serializer.LoadJson(req.request_body)
185       except Exception:
186         raise http.HttpBadRequest(message="Unable to parse JSON data")
187     else:
188       ctx.body_data = None
189
190     try:
191       result = ctx.handler_fn()
192     except luxi.TimeoutError:
193       raise http.HttpGatewayTimeout()
194     except luxi.ProtocolError, err:
195       raise http.HttpBadGateway(str(err))
196     except:
197       method = req.request_method.upper()
198       logging.exception("Error while handling the %s request", method)
199       raise
200
201     req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
202
203     return serializer.DumpJson(result)
204
205
206 def CheckRapi(options, args):
207   """Initial checks whether to run or exit with a failure.
208
209   """
210   if args: # rapi doesn't take any arguments
211     print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
212                           sys.argv[0])
213     sys.exit(constants.EXIT_FAILURE)
214
215   ssconf.CheckMaster(options.debug)
216
217   # Read SSL certificate (this is a little hackish to read the cert as root)
218   if options.ssl:
219     options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
220                                             ssl_cert_path=options.ssl_cert)
221   else:
222     options.ssl_params = None
223
224
225 def ExecRapi(options, _):
226   """Main remote API function, executed with the PID file held.
227
228   """
229
230   mainloop = daemon.Mainloop()
231   server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
232                                ssl_params=options.ssl_params,
233                                ssl_verify_peer=False,
234                                request_executor_class=JsonErrorRequestExecutor)
235   # pylint: disable-msg=E1101
236   # it seems pylint doesn't see the second parent class there
237   server.Start()
238   try:
239     mainloop.Run()
240   finally:
241     server.Stop()
242
243
244 def main():
245   """Main function.
246
247   """
248   parser = optparse.OptionParser(description="Ganeti Remote API",
249                     usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
250                     version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
251
252   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
253   dirs.append((constants.LOG_OS_DIR, 0750))
254   daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
255                      default_ssl_cert=constants.RAPI_CERT_FILE,
256                      default_ssl_key=constants.RAPI_CERT_FILE,
257                      user=constants.RAPI_USER, group=constants.DAEMONS_GROUP)
258
259
260 if __name__ == "__main__":
261   main()