Statistics
| Branch: | Tag: | Revision:

root / lib / server / rapi.py @ d9c82a4e

History | View | Annotate | Download (10.1 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
import errno
35

    
36
try:
37
  from pyinotify import pyinotify # pylint: disable-msg=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.rapi import connector
51

    
52
import ganeti.http.auth   # pylint: disable-msg=W0611
53
import ganeti.http.server
54

    
55

    
56
class RemoteApiRequestContext(object):
57
  """Data structure for Remote API requests.
58

59
  """
60
  def __init__(self):
61
    self.handler = None
62
    self.handler_fn = None
63
    self.handler_access = None
64
    self.body_data = None
65

    
66

    
67
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
68
  """Custom Request Executor class that formats HTTP errors in JSON.
69

70
  """
71
  error_content_type = http.HTTP_APP_JSON
72

    
73
  def _FormatErrorMessage(self, values):
74
    """Formats the body of an error message.
75

76
    @type values: dict
77
    @param values: dictionary with keys code, message and explain.
78
    @rtype: string
79
    @return: the body of the message
80

81
    """
82
    return serializer.DumpJson(values, indent=True)
83

    
84

    
85
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
86
                          http.server.HttpServer):
87
  """REST Request Handler Class.
88

89
  """
90
  AUTH_REALM = "Ganeti Remote API"
91

    
92
  def __init__(self, *args, **kwargs):
93
    # pylint: disable-msg=W0233
94
    # it seems pylint doesn't see the second parent class there
95
    http.server.HttpServer.__init__(self, *args, **kwargs)
96
    http.auth.HttpServerRequestAuthentication.__init__(self)
97
    self._resmap = connector.Mapper()
98
    self._users = None
99

    
100
  def LoadUsers(self, filename):
101
    """Loads a file containing users and passwords.
102

103
    @type filename: string
104
    @param filename: Path to file
105

106
    """
107
    logging.info("Reading users file at %s", filename)
108
    try:
109
      try:
110
        contents = utils.ReadFile(filename)
111
      except EnvironmentError, err:
112
        self._users = None
113
        if err.errno == errno.ENOENT:
114
          logging.warning("No users file at %s", filename)
115
        else:
116
          logging.warning("Error while reading %s: %s", filename, err)
117
        return False
118

    
119
      users = http.auth.ParsePasswordFile(contents)
120

    
121
    except Exception, err: # pylint: disable-msg=W0703
122
      # We don't care about the type of exception
123
      logging.error("Error while parsing %s: %s", filename, err)
124
      return False
125

    
126
    self._users = users
127

    
128
    return True
129

    
130
  def _GetRequestContext(self, req):
131
    """Returns the context for a request.
132

133
    The context is cached in the req.private variable.
134

135
    """
136
    if req.private is None:
137
      (HandlerClass, items, args) = \
138
                     self._resmap.getController(req.request_path)
139

    
140
      ctx = RemoteApiRequestContext()
141
      ctx.handler = HandlerClass(items, args, req)
142

    
143
      method = req.request_method.upper()
144
      try:
145
        ctx.handler_fn = getattr(ctx.handler, method)
146
      except AttributeError:
147
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
148
                                      (method, req.request_path))
149

    
150
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
151

    
152
      # Require permissions definition (usually in the base class)
153
      if ctx.handler_access is None:
154
        raise AssertionError("Permissions definition missing")
155

    
156
      # This is only made available in HandleRequest
157
      ctx.body_data = None
158

    
159
      req.private = ctx
160

    
161
    # Check for expected attributes
162
    assert req.private.handler
163
    assert req.private.handler_fn
164
    assert req.private.handler_access is not None
165

    
166
    return req.private
167

    
168
  def AuthenticationRequired(self, req):
169
    """Determine whether authentication is required.
170

171
    """
172
    return bool(self._GetRequestContext(req).handler_access)
173

    
174
  def Authenticate(self, req, username, password):
175
    """Checks whether a user can access a resource.
176

177
    """
178
    ctx = self._GetRequestContext(req)
179

    
180
    # Check username and password
181
    valid_user = False
182
    if self._users:
183
      user = self._users.get(username, None)
184
      if user and self.VerifyBasicAuthPassword(req, username, password,
185
                                               user.password):
186
        valid_user = True
187

    
188
    if not valid_user:
189
      # Unknown user or password wrong
190
      return False
191

    
192
    if (not ctx.handler_access or
193
        set(user.options).intersection(ctx.handler_access)):
194
      # Allow access
195
      return True
196

    
197
    # Access forbidden
198
    raise http.HttpForbidden()
199

    
200
  def HandleRequest(self, req):
201
    """Handles a request.
202

203
    """
204
    ctx = self._GetRequestContext(req)
205

    
206
    # Deserialize request parameters
207
    if req.request_body:
208
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
209
      # include a Content-Type header field defining the media type of that
210
      # body. [...] If the media type remains unknown, the recipient SHOULD
211
      # treat it as type "application/octet-stream".
212
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
213
                                                 http.HTTP_APP_OCTET_STREAM)
214
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
215
        raise http.HttpUnsupportedMediaType()
216

    
217
      try:
218
        ctx.body_data = serializer.LoadJson(req.request_body)
219
      except Exception:
220
        raise http.HttpBadRequest(message="Unable to parse JSON data")
221
    else:
222
      ctx.body_data = None
223

    
224
    try:
225
      result = ctx.handler_fn()
226
    except luxi.TimeoutError:
227
      raise http.HttpGatewayTimeout()
228
    except luxi.ProtocolError, err:
229
      raise http.HttpBadGateway(str(err))
230
    except:
231
      method = req.request_method.upper()
232
      logging.exception("Error while handling the %s request", method)
233
      raise
234

    
235
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
236

    
237
    return serializer.DumpJson(result)
238

    
239

    
240
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
241
  def __init__(self, wm, path, cb):
242
    """Initializes this class.
243

244
    @param wm: Inotify watch manager
245
    @type path: string
246
    @param path: File path
247
    @type cb: callable
248
    @param cb: Function called on file change
249

250
    """
251
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
252

    
253
    self._cb = cb
254
    self._filename = os.path.basename(path)
255

    
256
    # Different Pyinotify versions have the flag constants at different places,
257
    # hence not accessing them directly
258
    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
259
            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
260
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
261
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
262

    
263
    self._handle = self.AddWatch(os.path.dirname(path), mask)
264

    
265
  def process_default(self, event):
266
    """Called upon inotify event.
267

268
    """
269
    if event.name == self._filename:
270
      logging.debug("Received inotify event %s", event)
271
      self._cb()
272

    
273

    
274
def SetupFileWatcher(filename, cb):
275
  """Configures an inotify watcher for a file.
276

277
  @type filename: string
278
  @param filename: File to watch
279
  @type cb: callable
280
  @param cb: Function called on file change
281

282
  """
283
  wm = pyinotify.WatchManager()
284
  handler = FileEventHandler(wm, filename, cb)
285
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
286

    
287

    
288
def CheckRapi(options, args):
289
  """Initial checks whether to run or exit with a failure.
290

291
  """
292
  if args: # rapi doesn't take any arguments
293
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
294
                          sys.argv[0])
295
    sys.exit(constants.EXIT_FAILURE)
296

    
297
  ssconf.CheckMaster(options.debug)
298

    
299
  # Read SSL certificate (this is a little hackish to read the cert as root)
300
  if options.ssl:
301
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
302
                                            ssl_cert_path=options.ssl_cert)
303
  else:
304
    options.ssl_params = None
305

    
306

    
307
def PrepRapi(options, _):
308
  """Prep remote API function, executed with the PID file held.
309

310
  """
311

    
312
  mainloop = daemon.Mainloop()
313
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
314
                               ssl_params=options.ssl_params,
315
                               ssl_verify_peer=False,
316
                               request_executor_class=JsonErrorRequestExecutor)
317

    
318
  # Setup file watcher (it'll be driven by asyncore)
319
  SetupFileWatcher(constants.RAPI_USERS_FILE,
320
                   compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
321

    
322
  server.LoadUsers(constants.RAPI_USERS_FILE)
323

    
324
  # pylint: disable-msg=E1101
325
  # it seems pylint doesn't see the second parent class there
326
  server.Start()
327

    
328
  return (mainloop, server)
329

    
330

    
331
def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
332
  """Main remote API function, executed with the PID file held.
333

334
  """
335
  (mainloop, server) = prep_data
336
  try:
337
    mainloop.Run()
338
  finally:
339
    server.Stop()
340

    
341

    
342
def Main():
343
  """Main function.
344

345
  """
346
  parser = optparse.OptionParser(description="Ganeti Remote API",
347
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
348
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
349

    
350
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
351
                     default_ssl_cert=constants.RAPI_CERT_FILE,
352
                     default_ssl_key=constants.RAPI_CERT_FILE)