1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Ganeti Remote API master script.
22
23 """
24
25
26
27
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
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
55 import ganeti.http.server
56
57
58 -class RemoteApiRequestContext(object):
59 """Data structure for Remote API requests.
60
61 """
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
87
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
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
130 if ctx.handler_access is None:
131 raise AssertionError("Permissions definition missing")
132
133
134 ctx.body_data = None
135
136 req.private = ctx
137
138
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
146 """Determine whether authentication is required.
147
148 """
149 return self._reqauth or bool(self._GetRequestContext(req).handler_access)
150
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
162 return False
163
164 if (not ctx.handler_access or
165 set(user.options).intersection(ctx.handler_access)):
166
167 return True
168
169
170 raise http.HttpForbidden()
171
206
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:
246
247 logging.error("Error while parsing %s: %s", filename, err)
248 return False
249
250 self._users = users
251
252 return True
253
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
272
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
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
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
320
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
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):
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
359 """Main function.
360
361 """
362 parser = optparse.OptionParser(description="Ganeti Remote API",
363 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
364 version="%%prog (ganeti) %s" %
365 constants.RELEASE_VERSION)
366 parser.add_option("--require-authentication", dest="reqauth",
367 default=False, action="store_true",
368 help=("Disable anonymous HTTP requests and require"
369 " authentication"))
370
371 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
372 default_ssl_cert=pathutils.RAPI_CERT_FILE,
373 default_ssl_key=pathutils.RAPI_CERT_FILE)
374