Remove XEN_CMD from constants, adjust to PowercycleNode
[ganeti-local] / lib / server / rapi.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013 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=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 import errno
35
36 try:
37   from pyinotify import pyinotify # pylint: disable=E0611
38 except ImportError:
39   import pyinotify
40
41 from ganeti import asyncnotifier
42 from ganeti import constants
43 from ganeti import http
44 from ganeti import daemon
45 from ganeti import ssconf
46 from ganeti import luxi
47 from ganeti import serializer
48 from ganeti import compat
49 from ganeti import utils
50 from ganeti import pathutils
51 from ganeti.rapi import connector
52 from ganeti.rapi import baserlib
53
54 import ganeti.http.auth   # pylint: disable=W0611
55 import ganeti.http.server
56
57
58 class RemoteApiRequestContext(object):
59   """Data structure for Remote API requests.
60
61   """
62   def __init__(self):
63     self.handler = None
64     self.handler_fn = None
65     self.handler_access = None
66     self.body_data = None
67
68
69 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
70                        http.server.HttpServerHandler):
71   """REST Request Handler Class.
72
73   """
74   AUTH_REALM = "Ganeti Remote API"
75
76   def __init__(self, user_fn, reqauth, _client_cls=None):
77     """Initializes this class.
78
79     @type user_fn: callable
80     @param user_fn: Function receiving username as string and returning
81       L{http.auth.PasswordFileUser} or C{None} if user is not found
82     @type reqauth: bool
83     @param reqauth: Whether to require authentication
84
85     """
86     # pylint: disable=W0233
87     # it seems pylint doesn't see the second parent class there
88     http.server.HttpServerHandler.__init__(self)
89     http.auth.HttpServerRequestAuthentication.__init__(self)
90     self._client_cls = _client_cls
91     self._resmap = connector.Mapper()
92     self._user_fn = user_fn
93     self._reqauth = reqauth
94
95   @staticmethod
96   def FormatErrorMessage(values):
97     """Formats the body of an error message.
98
99     @type values: dict
100     @param values: dictionary with keys C{code}, C{message} and C{explain}.
101     @rtype: tuple; (string, string)
102     @return: Content-type and response body
103
104     """
105     return (http.HTTP_APP_JSON, serializer.DumpJson(values))
106
107   def _GetRequestContext(self, req):
108     """Returns the context for a request.
109
110     The context is cached in the req.private variable.
111
112     """
113     if req.private is None:
114       (HandlerClass, items, args) = \
115                      self._resmap.getController(req.request_path)
116
117       ctx = RemoteApiRequestContext()
118       ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
119
120       method = req.request_method.upper()
121       try:
122         ctx.handler_fn = getattr(ctx.handler, method)
123       except AttributeError:
124         raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
125                                       (method, req.request_path))
126
127       ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method)
128
129       # Require permissions definition (usually in the base class)
130       if ctx.handler_access is None:
131         raise AssertionError("Permissions definition missing")
132
133       # This is only made available in HandleRequest
134       ctx.body_data = None
135
136       req.private = ctx
137
138     # Check for expected attributes
139     assert req.private.handler
140     assert req.private.handler_fn
141     assert req.private.handler_access is not None
142
143     return req.private
144
145   def AuthenticationRequired(self, req):
146     """Determine whether authentication is required.
147
148     """
149     return self._reqauth or bool(self._GetRequestContext(req).handler_access)
150
151   def Authenticate(self, req, username, password):
152     """Checks whether a user can access a resource.
153
154     """
155     ctx = self._GetRequestContext(req)
156
157     user = self._user_fn(username)
158     if not (user and
159             self.VerifyBasicAuthPassword(req, username, password,
160                                          user.password)):
161       # Unknown user or password wrong
162       return False
163
164     if (not ctx.handler_access or
165         set(user.options).intersection(ctx.handler_access)):
166       # Allow access
167       return True
168
169     # Access forbidden
170     raise http.HttpForbidden()
171
172   def HandleRequest(self, req):
173     """Handles a request.
174
175     """
176     ctx = self._GetRequestContext(req)
177
178     # Deserialize request parameters
179     if req.request_body:
180       # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
181       # include a Content-Type header field defining the media type of that
182       # body. [...] If the media type remains unknown, the recipient SHOULD
183       # treat it as type "application/octet-stream".
184       req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
185                                                  http.HTTP_APP_OCTET_STREAM)
186       if req_content_type.lower() != http.HTTP_APP_JSON.lower():
187         raise http.HttpUnsupportedMediaType()
188
189       try:
190         ctx.body_data = serializer.LoadJson(req.request_body)
191       except Exception:
192         raise http.HttpBadRequest(message="Unable to parse JSON data")
193     else:
194       ctx.body_data = None
195
196     try:
197       result = ctx.handler_fn()
198     except luxi.TimeoutError:
199       raise http.HttpGatewayTimeout()
200     except luxi.ProtocolError, err:
201       raise http.HttpBadGateway(str(err))
202
203     req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
204
205     return serializer.DumpJson(result)
206
207
208 class RapiUsers:
209   def __init__(self):
210     """Initializes this class.
211
212     """
213     self._users = None
214
215   def Get(self, username):
216     """Checks whether a user exists.
217
218     """
219     if self._users:
220       return self._users.get(username, None)
221     else:
222       return None
223
224   def Load(self, filename):
225     """Loads a file containing users and passwords.
226
227     @type filename: string
228     @param filename: Path to file
229
230     """
231     logging.info("Reading users file at %s", filename)
232     try:
233       try:
234         contents = utils.ReadFile(filename)
235       except EnvironmentError, err:
236         self._users = None
237         if err.errno == errno.ENOENT:
238           logging.warning("No users file at %s", filename)
239         else:
240           logging.warning("Error while reading %s: %s", filename, err)
241         return False
242
243       users = http.auth.ParsePasswordFile(contents)
244
245     except Exception, err: # pylint: disable=W0703
246       # We don't care about the type of exception
247       logging.error("Error while parsing %s: %s", filename, err)
248       return False
249
250     self._users = users
251
252     return True
253
254
255 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
256   def __init__(self, wm, path, cb):
257     """Initializes this class.
258
259     @param wm: Inotify watch manager
260     @type path: string
261     @param path: File path
262     @type cb: callable
263     @param cb: Function called on file change
264
265     """
266     asyncnotifier.FileEventHandlerBase.__init__(self, wm)
267
268     self._cb = cb
269     self._filename = os.path.basename(path)
270
271     # Different Pyinotify versions have the flag constants at different places,
272     # hence not accessing them directly
273     mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
274             pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
275             pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
276             pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
277
278     self._handle = self.AddWatch(os.path.dirname(path), mask)
279
280   def process_default(self, event):
281     """Called upon inotify event.
282
283     """
284     if event.name == self._filename:
285       logging.debug("Received inotify event %s", event)
286       self._cb()
287
288
289 def SetupFileWatcher(filename, cb):
290   """Configures an inotify watcher for a file.
291
292   @type filename: string
293   @param filename: File to watch
294   @type cb: callable
295   @param cb: Function called on file change
296
297   """
298   wm = pyinotify.WatchManager()
299   handler = FileEventHandler(wm, filename, cb)
300   asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
301
302
303 def CheckRapi(options, args):
304   """Initial checks whether to run or exit with a failure.
305
306   """
307   if args: # rapi doesn't take any arguments
308     print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
309                           sys.argv[0])
310     sys.exit(constants.EXIT_FAILURE)
311
312   ssconf.CheckMaster(options.debug)
313
314   # Read SSL certificate (this is a little hackish to read the cert as root)
315   if options.ssl:
316     options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
317                                             ssl_cert_path=options.ssl_cert)
318   else:
319     options.ssl_params = None
320
321
322 def PrepRapi(options, _):
323   """Prep remote API function, executed with the PID file held.
324
325   """
326   mainloop = daemon.Mainloop()
327
328   users = RapiUsers()
329
330   handler = RemoteApiHandler(users.Get, options.reqauth)
331
332   # Setup file watcher (it'll be driven by asyncore)
333   SetupFileWatcher(pathutils.RAPI_USERS_FILE,
334                    compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
335
336   users.Load(pathutils.RAPI_USERS_FILE)
337
338   server = \
339     http.server.HttpServer(mainloop, options.bind_address, options.port,
340                            handler,
341                            ssl_params=options.ssl_params, ssl_verify_peer=False)
342   server.Start()
343
344   return (mainloop, server)
345
346
347 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
348   """Main remote API function, executed with the PID file held.
349
350   """
351   (mainloop, server) = prep_data
352   try:
353     mainloop.Run()
354   finally:
355     server.Stop()
356
357
358 def Main():
359   """Main function.
360
361   """
362   parser = optparse.OptionParser(description="Ganeti Remote API",
363                                  usage="%prog [-f] [-d] [-p port] [-b ADDRESS]\
364                                        \ [-i INTERFACE]",
365                                  version="%%prog (ganeti) %s" %
366                                  constants.RELEASE_VERSION)
367   parser.add_option("--require-authentication", dest="reqauth",
368                     default=False, action="store_true",
369                     help=("Disable anonymous HTTP requests and require"
370                           " authentication"))
371
372   daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
373                      default_ssl_cert=pathutils.RAPI_CERT_FILE,
374                      default_ssl_key=pathutils.RAPI_CERT_FILE)