Statistics
| Branch: | Tag: | Revision:

root / lib / server / rapi.py @ 377ae13e

History | View | Annotate | Download (9.7 kB)

1
#
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=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.rapi import connector
51

    
52
import ganeti.http.auth   # pylint: disable=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 RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
68
                       http.server.HttpServerHandler):
69
  """REST Request Handler Class.
70

71
  """
72
  AUTH_REALM = "Ganeti Remote API"
73

    
74
  def __init__(self):
75
    # pylint: disable=W0233
76
    # it seems pylint doesn't see the second parent class there
77
    http.server.HttpServerHandler.__init__(self)
78
    http.auth.HttpServerRequestAuthentication.__init__(self)
79
    self._resmap = connector.Mapper()
80
    self._users = None
81

    
82
  def LoadUsers(self, filename):
83
    """Loads a file containing users and passwords.
84

85
    @type filename: string
86
    @param filename: Path to file
87

88
    """
89
    logging.info("Reading users file at %s", filename)
90
    try:
91
      try:
92
        contents = utils.ReadFile(filename)
93
      except EnvironmentError, err:
94
        self._users = None
95
        if err.errno == errno.ENOENT:
96
          logging.warning("No users file at %s", filename)
97
        else:
98
          logging.warning("Error while reading %s: %s", filename, err)
99
        return False
100

    
101
      users = http.auth.ParsePasswordFile(contents)
102

    
103
    except Exception, err: # pylint: disable=W0703
104
      # We don't care about the type of exception
105
      logging.error("Error while parsing %s: %s", filename, err)
106
      return False
107

    
108
    self._users = users
109

    
110
    return True
111

    
112
  @staticmethod
113
  def FormatErrorMessage(values):
114
    """Formats the body of an error message.
115

116
    @type values: dict
117
    @param values: dictionary with keys C{code}, C{message} and C{explain}.
118
    @rtype: tuple; (string, string)
119
    @return: Content-type and response body
120

121
    """
122
    return (http.HTTP_APP_JSON, serializer.DumpJson(values))
123

    
124
  def _GetRequestContext(self, req):
125
    """Returns the context for a request.
126

127
    The context is cached in the req.private variable.
128

129
    """
130
    if req.private is None:
131
      (HandlerClass, items, args) = \
132
                     self._resmap.getController(req.request_path)
133

    
134
      ctx = RemoteApiRequestContext()
135
      ctx.handler = HandlerClass(items, args, req)
136

    
137
      method = req.request_method.upper()
138
      try:
139
        ctx.handler_fn = getattr(ctx.handler, method)
140
      except AttributeError:
141
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
142
                                      (method, req.request_path))
143

    
144
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
145

    
146
      # Require permissions definition (usually in the base class)
147
      if ctx.handler_access is None:
148
        raise AssertionError("Permissions definition missing")
149

    
150
      # This is only made available in HandleRequest
151
      ctx.body_data = None
152

    
153
      req.private = ctx
154

    
155
    # Check for expected attributes
156
    assert req.private.handler
157
    assert req.private.handler_fn
158
    assert req.private.handler_access is not None
159

    
160
    return req.private
161

    
162
  def AuthenticationRequired(self, req):
163
    """Determine whether authentication is required.
164

165
    """
166
    return bool(self._GetRequestContext(req).handler_access)
167

    
168
  def Authenticate(self, req, username, password):
169
    """Checks whether a user can access a resource.
170

171
    """
172
    ctx = self._GetRequestContext(req)
173

    
174
    # Check username and password
175
    valid_user = False
176
    if self._users:
177
      user = self._users.get(username, None)
178
      if user and self.VerifyBasicAuthPassword(req, username, password,
179
                                               user.password):
180
        valid_user = True
181

    
182
    if not valid_user:
183
      # Unknown user or password wrong
184
      return False
185

    
186
    if (not ctx.handler_access or
187
        set(user.options).intersection(ctx.handler_access)):
188
      # Allow access
189
      return True
190

    
191
    # Access forbidden
192
    raise http.HttpForbidden()
193

    
194
  def HandleRequest(self, req):
195
    """Handles a request.
196

197
    """
198
    ctx = self._GetRequestContext(req)
199

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

    
211
      try:
212
        ctx.body_data = serializer.LoadJson(req.request_body)
213
      except Exception:
214
        raise http.HttpBadRequest(message="Unable to parse JSON data")
215
    else:
216
      ctx.body_data = None
217

    
218
    try:
219
      result = ctx.handler_fn()
220
    except luxi.TimeoutError:
221
      raise http.HttpGatewayTimeout()
222
    except luxi.ProtocolError, err:
223
      raise http.HttpBadGateway(str(err))
224
    except:
225
      method = req.request_method.upper()
226
      logging.exception("Error while handling the %s request", method)
227
      raise
228

    
229
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
230

    
231
    return serializer.DumpJson(result)
232

    
233

    
234
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
235
  def __init__(self, wm, path, cb):
236
    """Initializes this class.
237

238
    @param wm: Inotify watch manager
239
    @type path: string
240
    @param path: File path
241
    @type cb: callable
242
    @param cb: Function called on file change
243

244
    """
245
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
246

    
247
    self._cb = cb
248
    self._filename = os.path.basename(path)
249

    
250
    # Different Pyinotify versions have the flag constants at different places,
251
    # hence not accessing them directly
252
    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
253
            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
254
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
255
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
256

    
257
    self._handle = self.AddWatch(os.path.dirname(path), mask)
258

    
259
  def process_default(self, event):
260
    """Called upon inotify event.
261

262
    """
263
    if event.name == self._filename:
264
      logging.debug("Received inotify event %s", event)
265
      self._cb()
266

    
267

    
268
def SetupFileWatcher(filename, cb):
269
  """Configures an inotify watcher for a file.
270

271
  @type filename: string
272
  @param filename: File to watch
273
  @type cb: callable
274
  @param cb: Function called on file change
275

276
  """
277
  wm = pyinotify.WatchManager()
278
  handler = FileEventHandler(wm, filename, cb)
279
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
280

    
281

    
282
def CheckRapi(options, args):
283
  """Initial checks whether to run or exit with a failure.
284

285
  """
286
  if args: # rapi doesn't take any arguments
287
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
288
                          sys.argv[0])
289
    sys.exit(constants.EXIT_FAILURE)
290

    
291
  ssconf.CheckMaster(options.debug)
292

    
293
  # Read SSL certificate (this is a little hackish to read the cert as root)
294
  if options.ssl:
295
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
296
                                            ssl_cert_path=options.ssl_cert)
297
  else:
298
    options.ssl_params = None
299

    
300

    
301
def PrepRapi(options, _):
302
  """Prep remote API function, executed with the PID file held.
303

304
  """
305
  mainloop = daemon.Mainloop()
306
  handler = RemoteApiHandler()
307

    
308
  # Setup file watcher (it'll be driven by asyncore)
309
  SetupFileWatcher(constants.RAPI_USERS_FILE,
310
                   compat.partial(handler.LoadUsers, constants.RAPI_USERS_FILE))
311

    
312
  handler.LoadUsers(constants.RAPI_USERS_FILE)
313

    
314
  server = \
315
    http.server.HttpServer(mainloop, options.bind_address, options.port,
316
      handler, ssl_params=options.ssl_params, ssl_verify_peer=False)
317
  server.Start()
318

    
319
  return (mainloop, server)
320

    
321

    
322
def ExecRapi(options, args, prep_data): # pylint: disable=W0613
323
  """Main remote API function, executed with the PID file held.
324

325
  """
326
  (mainloop, server) = prep_data
327
  try:
328
    mainloop.Run()
329
  finally:
330
    server.Stop()
331

    
332

    
333
def Main():
334
  """Main function.
335

336
  """
337
  parser = optparse.OptionParser(description="Ganeti Remote API",
338
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
339
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
340

    
341
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
342
                     default_ssl_cert=constants.RAPI_CERT_FILE,
343
                     default_ssl_key=constants.RAPI_CERT_FILE)