4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013 Google Inc.
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.
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.
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
21 """Ganeti Remote API master script.
25 # pylint: disable=C0103,W0142
27 # C0103: Invalid name ganeti-watcher
37 from pyinotify import pyinotify # pylint: disable=E0611
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
54 import ganeti.http.auth # pylint: disable=W0611
55 import ganeti.http.server
58 class RemoteApiRequestContext(object):
59 """Data structure for Remote API requests.
64 self.handler_fn = None
65 self.handler_access = None
69 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
70 http.server.HttpServerHandler):
71 """REST Request Handler Class.
74 AUTH_REALM = "Ganeti Remote API"
76 def __init__(self, user_fn, _client_cls=None):
77 """Initializes this class.
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
84 # pylint: disable=W0233
85 # it seems pylint doesn't see the second parent class there
86 http.server.HttpServerHandler.__init__(self)
87 http.auth.HttpServerRequestAuthentication.__init__(self)
88 self._client_cls = _client_cls
89 self._resmap = connector.Mapper()
90 self._user_fn = user_fn
93 def FormatErrorMessage(values):
94 """Formats the body of an error message.
97 @param values: dictionary with keys C{code}, C{message} and C{explain}.
98 @rtype: tuple; (string, string)
99 @return: Content-type and response body
102 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
104 def _GetRequestContext(self, req):
105 """Returns the context for a request.
107 The context is cached in the req.private variable.
110 if req.private is None:
111 (HandlerClass, items, args) = \
112 self._resmap.getController(req.request_path)
114 ctx = RemoteApiRequestContext()
115 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
117 method = req.request_method.upper()
119 ctx.handler_fn = getattr(ctx.handler, method)
120 except AttributeError:
121 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
122 (method, req.request_path))
124 ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method)
126 # Require permissions definition (usually in the base class)
127 if ctx.handler_access is None:
128 raise AssertionError("Permissions definition missing")
130 # This is only made available in HandleRequest
135 # Check for expected attributes
136 assert req.private.handler
137 assert req.private.handler_fn
138 assert req.private.handler_access is not None
142 def AuthenticationRequired(self, req):
143 """Determine whether authentication is required.
146 return bool(self._GetRequestContext(req).handler_access)
148 def Authenticate(self, req, username, password):
149 """Checks whether a user can access a resource.
152 ctx = self._GetRequestContext(req)
154 user = self._user_fn(username)
156 self.VerifyBasicAuthPassword(req, username, password,
158 # Unknown user or password wrong
161 if (not ctx.handler_access or
162 set(user.options).intersection(ctx.handler_access)):
167 raise http.HttpForbidden()
169 def HandleRequest(self, req):
170 """Handles a request.
173 ctx = self._GetRequestContext(req)
175 # Deserialize request parameters
177 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
178 # include a Content-Type header field defining the media type of that
179 # body. [...] If the media type remains unknown, the recipient SHOULD
180 # treat it as type "application/octet-stream".
181 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
182 http.HTTP_APP_OCTET_STREAM)
183 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
184 raise http.HttpUnsupportedMediaType()
187 ctx.body_data = serializer.LoadJson(req.request_body)
189 raise http.HttpBadRequest(message="Unable to parse JSON data")
194 result = ctx.handler_fn()
195 except luxi.TimeoutError:
196 raise http.HttpGatewayTimeout()
197 except luxi.ProtocolError, err:
198 raise http.HttpBadGateway(str(err))
200 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
202 return serializer.DumpJson(result)
207 """Initializes this class.
212 def Get(self, username):
213 """Checks whether a user exists.
217 return self._users.get(username, None)
221 def Load(self, filename):
222 """Loads a file containing users and passwords.
224 @type filename: string
225 @param filename: Path to file
228 logging.info("Reading users file at %s", filename)
231 contents = utils.ReadFile(filename)
232 except EnvironmentError, err:
234 if err.errno == errno.ENOENT:
235 logging.warning("No users file at %s", filename)
237 logging.warning("Error while reading %s: %s", filename, err)
240 users = http.auth.ParsePasswordFile(contents)
242 except Exception, err: # pylint: disable=W0703
243 # We don't care about the type of exception
244 logging.error("Error while parsing %s: %s", filename, err)
252 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
253 def __init__(self, wm, path, cb):
254 """Initializes this class.
256 @param wm: Inotify watch manager
258 @param path: File path
260 @param cb: Function called on file change
263 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
266 self._filename = os.path.basename(path)
268 # Different Pyinotify versions have the flag constants at different places,
269 # hence not accessing them directly
270 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
271 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
272 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
273 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
275 self._handle = self.AddWatch(os.path.dirname(path), mask)
277 def process_default(self, event):
278 """Called upon inotify event.
281 if event.name == self._filename:
282 logging.debug("Received inotify event %s", event)
286 def SetupFileWatcher(filename, cb):
287 """Configures an inotify watcher for a file.
289 @type filename: string
290 @param filename: File to watch
292 @param cb: Function called on file change
295 wm = pyinotify.WatchManager()
296 handler = FileEventHandler(wm, filename, cb)
297 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
300 def CheckRapi(options, args):
301 """Initial checks whether to run or exit with a failure.
304 if args: # rapi doesn't take any arguments
305 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
307 sys.exit(constants.EXIT_FAILURE)
309 ssconf.CheckMaster(options.debug)
311 # Read SSL certificate (this is a little hackish to read the cert as root)
313 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
314 ssl_cert_path=options.ssl_cert)
316 options.ssl_params = None
319 def PrepRapi(options, _):
320 """Prep remote API function, executed with the PID file held.
323 mainloop = daemon.Mainloop()
327 handler = RemoteApiHandler(users.Get)
329 # Setup file watcher (it'll be driven by asyncore)
330 SetupFileWatcher(pathutils.RAPI_USERS_FILE,
331 compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
333 users.Load(pathutils.RAPI_USERS_FILE)
336 http.server.HttpServer(mainloop, options.bind_address, options.port,
338 ssl_params=options.ssl_params, ssl_verify_peer=False)
341 return (mainloop, server)
344 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
345 """Main remote API function, executed with the PID file held.
348 (mainloop, server) = prep_data
359 parser = optparse.OptionParser(description="Ganeti Remote API",
360 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
361 version="%%prog (ganeti) %s" %
362 constants.RELEASE_VERSION)
364 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
365 default_ssl_cert=pathutils.RAPI_CERT_FILE,
366 default_ssl_key=pathutils.RAPI_CERT_FILE)