1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 """Ganeti Remote API master script.
31
32 """
33
34
35
36
37
38 import logging
39 import optparse
40 import sys
41 import os
42 import os.path
43 import errno
44
45 try:
46 from pyinotify import pyinotify
47 except ImportError:
48 import pyinotify
49
50 from ganeti import asyncnotifier
51 from ganeti import constants
52 from ganeti import http
53 from ganeti import daemon
54 from ganeti import ssconf
55 from ganeti import luxi
56 from ganeti import serializer
57 from ganeti import compat
58 from ganeti import utils
59 from ganeti import pathutils
60 from ganeti.rapi import connector
61 from ganeti.rapi import baserlib
62
63 import ganeti.http.auth
64 import ganeti.http.server
65
66
67 -class RemoteApiRequestContext(object):
68 """Data structure for Remote API requests.
69
70 """
72 self.handler = None
73 self.handler_fn = None
74 self.handler_access = None
75 self.body_data = None
76
77
78 -class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
79 http.server.HttpServerHandler):
80 """REST Request Handler Class.
81
82 """
83 AUTH_REALM = "Ganeti Remote API"
84
85 - def __init__(self, user_fn, reqauth, _client_cls=None):
86 """Initializes this class.
87
88 @type user_fn: callable
89 @param user_fn: Function receiving username as string and returning
90 L{http.auth.PasswordFileUser} or C{None} if user is not found
91 @type reqauth: bool
92 @param reqauth: Whether to require authentication
93
94 """
95
96
97 http.server.HttpServerHandler.__init__(self)
98 http.auth.HttpServerRequestAuthentication.__init__(self)
99 self._client_cls = _client_cls
100 self._resmap = connector.Mapper()
101 self._user_fn = user_fn
102 self._reqauth = reqauth
103
104 @staticmethod
115
116 - def _GetRequestContext(self, req):
117 """Returns the context for a request.
118
119 The context is cached in the req.private variable.
120
121 """
122 if req.private is None:
123 (HandlerClass, items, args) = \
124 self._resmap.getController(req.request_path)
125
126 ctx = RemoteApiRequestContext()
127 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
128
129 method = req.request_method.upper()
130 try:
131 ctx.handler_fn = getattr(ctx.handler, method)
132 except AttributeError:
133 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
134 (method, req.request_path))
135
136 ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method)
137
138
139 if ctx.handler_access is None:
140 raise AssertionError("Permissions definition missing")
141
142
143 ctx.body_data = None
144
145 req.private = ctx
146
147
148 assert req.private.handler
149 assert req.private.handler_fn
150 assert req.private.handler_access is not None
151
152 return req.private
153
155 """Determine whether authentication is required.
156
157 """
158 return self._reqauth or bool(self._GetRequestContext(req).handler_access)
159
161 """Checks whether a user can access a resource.
162
163 """
164 ctx = self._GetRequestContext(req)
165
166 user = self._user_fn(username)
167 if not (user and
168 self.VerifyBasicAuthPassword(req, username, password,
169 user.password)):
170
171 return False
172
173 if (not ctx.handler_access or
174 set(user.options).intersection(ctx.handler_access)):
175
176 return True
177
178
179 raise http.HttpForbidden()
180
215
219 """Initializes this class.
220
221 """
222 self._users = None
223
224 - def Get(self, username):
225 """Checks whether a user exists.
226
227 """
228 if self._users:
229 return self._users.get(username, None)
230 else:
231 return None
232
233 - def Load(self, filename):
234 """Loads a file containing users and passwords.
235
236 @type filename: string
237 @param filename: Path to file
238
239 """
240 logging.info("Reading users file at %s", filename)
241 try:
242 try:
243 contents = utils.ReadFile(filename)
244 except EnvironmentError, err:
245 self._users = None
246 if err.errno == errno.ENOENT:
247 logging.warning("No users file at %s", filename)
248 else:
249 logging.warning("Error while reading %s: %s", filename, err)
250 return False
251
252 users = http.auth.ParsePasswordFile(contents)
253
254 except Exception, err:
255
256 logging.error("Error while parsing %s: %s", filename, err)
257 return False
258
259 self._users = users
260
261 return True
262
266 """Initializes this class.
267
268 @param wm: Inotify watch manager
269 @type path: string
270 @param path: File path
271 @type cb: callable
272 @param cb: Function called on file change
273
274 """
275 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
276
277 self._cb = cb
278 self._filename = os.path.basename(path)
279
280
281
282 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
283 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
284 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
285 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
286
287 self._handle = self.AddWatch(os.path.dirname(path), mask)
288
290 """Called upon inotify event.
291
292 """
293 if event.name == self._filename:
294 logging.debug("Received inotify event %s", event)
295 self._cb()
296
299 """Configures an inotify watcher for a file.
300
301 @type filename: string
302 @param filename: File to watch
303 @type cb: callable
304 @param cb: Function called on file change
305
306 """
307 wm = pyinotify.WatchManager()
308 handler = FileEventHandler(wm, filename, cb)
309 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
310
329
332 """Prep remote API function, executed with the PID file held.
333
334 """
335 mainloop = daemon.Mainloop()
336
337 users = RapiUsers()
338
339 handler = RemoteApiHandler(users.Get, options.reqauth)
340
341
342 SetupFileWatcher(pathutils.RAPI_USERS_FILE,
343 compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
344
345 users.Load(pathutils.RAPI_USERS_FILE)
346
347 server = \
348 http.server.HttpServer(mainloop, options.bind_address, options.port,
349 handler,
350 ssl_params=options.ssl_params, ssl_verify_peer=False)
351 server.Start()
352
353 return (mainloop, server)
354
355
356 -def ExecRapi(options, args, prep_data):
357 """Main remote API function, executed with the PID file held.
358
359 """
360 (mainloop, server) = prep_data
361 try:
362 mainloop.Run()
363 finally:
364 server.Stop()
365
368 """Main function.
369
370 """
371 parser = optparse.OptionParser(description="Ganeti Remote API",
372 usage=("%prog [-f] [-d] [-p port] [-b ADDRESS]"
373 " [-i INTERFACE]"),
374 version="%%prog (ganeti) %s" %
375 constants.RELEASE_VERSION)
376 parser.add_option("--require-authentication", dest="reqauth",
377 default=False, action="store_true",
378 help=("Disable anonymous HTTP requests and require"
379 " authentication"))
380
381 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
382 default_ssl_cert=pathutils.RAPI_CERT_FILE,
383 default_ssl_key=pathutils.RAPI_CERT_FILE)
384