Statistics
| Branch: | Tag: | Revision:

root / lib / server / rapi.py @ 5ae4945a

History | View | Annotate | Download (10.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012 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, user_fn, _client_cls=None):
75
    """Initializes this class.
76

77
    @type user_fn: callable
78
    @param user_fn: Function receiving username as string and returning
79
      L{http.auth.PasswordFileUser} or C{None} if user is not found
80

81
    """
82
    # pylint: disable=W0233
83
    # it seems pylint doesn't see the second parent class there
84
    http.server.HttpServerHandler.__init__(self)
85
    http.auth.HttpServerRequestAuthentication.__init__(self)
86
    self._client_cls = _client_cls
87
    self._resmap = connector.Mapper()
88
    self._user_fn = user_fn
89

    
90
  @staticmethod
91
  def FormatErrorMessage(values):
92
    """Formats the body of an error message.
93

94
    @type values: dict
95
    @param values: dictionary with keys C{code}, C{message} and C{explain}.
96
    @rtype: tuple; (string, string)
97
    @return: Content-type and response body
98

99
    """
100
    return (http.HTTP_APP_JSON, serializer.DumpJson(values))
101

    
102
  def _GetRequestContext(self, req):
103
    """Returns the context for a request.
104

105
    The context is cached in the req.private variable.
106

107
    """
108
    if req.private is None:
109
      (HandlerClass, items, args) = \
110
                     self._resmap.getController(req.request_path)
111

    
112
      ctx = RemoteApiRequestContext()
113
      ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
114

    
115
      method = req.request_method.upper()
116
      try:
117
        ctx.handler_fn = getattr(ctx.handler, method)
118
      except AttributeError:
119
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
120
                                      (method, req.request_path))
121

    
122
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
123

    
124
      # Require permissions definition (usually in the base class)
125
      if ctx.handler_access is None:
126
        raise AssertionError("Permissions definition missing")
127

    
128
      # This is only made available in HandleRequest
129
      ctx.body_data = None
130

    
131
      req.private = ctx
132

    
133
    # Check for expected attributes
134
    assert req.private.handler
135
    assert req.private.handler_fn
136
    assert req.private.handler_access is not None
137

    
138
    return req.private
139

    
140
  def AuthenticationRequired(self, req):
141
    """Determine whether authentication is required.
142

143
    """
144
    return bool(self._GetRequestContext(req).handler_access)
145

    
146
  def Authenticate(self, req, username, password):
147
    """Checks whether a user can access a resource.
148

149
    """
150
    ctx = self._GetRequestContext(req)
151

    
152
    user = self._user_fn(username)
153
    if not (user and
154
            self.VerifyBasicAuthPassword(req, username, password,
155
                                         user.password)):
156
      # Unknown user or password wrong
157
      return False
158

    
159
    if (not ctx.handler_access or
160
        set(user.options).intersection(ctx.handler_access)):
161
      # Allow access
162
      return True
163

    
164
    # Access forbidden
165
    raise http.HttpForbidden()
166

    
167
  def HandleRequest(self, req):
168
    """Handles a request.
169

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

    
173
    # Deserialize request parameters
174
    if req.request_body:
175
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
176
      # include a Content-Type header field defining the media type of that
177
      # body. [...] If the media type remains unknown, the recipient SHOULD
178
      # treat it as type "application/octet-stream".
179
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
180
                                                 http.HTTP_APP_OCTET_STREAM)
181
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
182
        raise http.HttpUnsupportedMediaType()
183

    
184
      try:
185
        ctx.body_data = serializer.LoadJson(req.request_body)
186
      except Exception:
187
        raise http.HttpBadRequest(message="Unable to parse JSON data")
188
    else:
189
      ctx.body_data = None
190

    
191
    try:
192
      result = ctx.handler_fn()
193
    except luxi.TimeoutError:
194
      raise http.HttpGatewayTimeout()
195
    except luxi.ProtocolError, err:
196
      raise http.HttpBadGateway(str(err))
197

    
198
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
199

    
200
    return serializer.DumpJson(result)
201

    
202

    
203
class RapiUsers:
204
  def __init__(self):
205
    """Initializes this class.
206

207
    """
208
    self._users = None
209

    
210
  def Get(self, username):
211
    """Checks whether a user exists.
212

213
    """
214
    if self._users:
215
      return self._users.get(username, None)
216
    else:
217
      return None
218

    
219
  def Load(self, filename):
220
    """Loads a file containing users and passwords.
221

222
    @type filename: string
223
    @param filename: Path to file
224

225
    """
226
    logging.info("Reading users file at %s", filename)
227
    try:
228
      try:
229
        contents = utils.ReadFile(filename)
230
      except EnvironmentError, err:
231
        self._users = None
232
        if err.errno == errno.ENOENT:
233
          logging.warning("No users file at %s", filename)
234
        else:
235
          logging.warning("Error while reading %s: %s", filename, err)
236
        return False
237

    
238
      users = http.auth.ParsePasswordFile(contents)
239

    
240
    except Exception, err: # pylint: disable=W0703
241
      # We don't care about the type of exception
242
      logging.error("Error while parsing %s: %s", filename, err)
243
      return False
244

    
245
    self._users = users
246

    
247
    return True
248

    
249

    
250
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
251
  def __init__(self, wm, path, cb):
252
    """Initializes this class.
253

254
    @param wm: Inotify watch manager
255
    @type path: string
256
    @param path: File path
257
    @type cb: callable
258
    @param cb: Function called on file change
259

260
    """
261
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
262

    
263
    self._cb = cb
264
    self._filename = os.path.basename(path)
265

    
266
    # Different Pyinotify versions have the flag constants at different places,
267
    # hence not accessing them directly
268
    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
269
            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
270
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
271
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
272

    
273
    self._handle = self.AddWatch(os.path.dirname(path), mask)
274

    
275
  def process_default(self, event):
276
    """Called upon inotify event.
277

278
    """
279
    if event.name == self._filename:
280
      logging.debug("Received inotify event %s", event)
281
      self._cb()
282

    
283

    
284
def SetupFileWatcher(filename, cb):
285
  """Configures an inotify watcher for a file.
286

287
  @type filename: string
288
  @param filename: File to watch
289
  @type cb: callable
290
  @param cb: Function called on file change
291

292
  """
293
  wm = pyinotify.WatchManager()
294
  handler = FileEventHandler(wm, filename, cb)
295
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
296

    
297

    
298
def CheckRapi(options, args):
299
  """Initial checks whether to run or exit with a failure.
300

301
  """
302
  if args: # rapi doesn't take any arguments
303
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
304
                          sys.argv[0])
305
    sys.exit(constants.EXIT_FAILURE)
306

    
307
  ssconf.CheckMaster(options.debug)
308

    
309
  # Read SSL certificate (this is a little hackish to read the cert as root)
310
  if options.ssl:
311
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
312
                                            ssl_cert_path=options.ssl_cert)
313
  else:
314
    options.ssl_params = None
315

    
316

    
317
def PrepRapi(options, _):
318
  """Prep remote API function, executed with the PID file held.
319

320
  """
321
  mainloop = daemon.Mainloop()
322

    
323
  users = RapiUsers()
324

    
325
  handler = RemoteApiHandler(users.Get)
326

    
327
  # Setup file watcher (it'll be driven by asyncore)
328
  SetupFileWatcher(constants.RAPI_USERS_FILE,
329
                   compat.partial(users.Load, constants.RAPI_USERS_FILE))
330

    
331
  users.Load(constants.RAPI_USERS_FILE)
332

    
333
  server = \
334
    http.server.HttpServer(mainloop, options.bind_address, options.port,
335
                           handler,
336
                           ssl_params=options.ssl_params, ssl_verify_peer=False)
337
  server.Start()
338

    
339
  return (mainloop, server)
340

    
341

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

345
  """
346
  (mainloop, server) = prep_data
347
  try:
348
    mainloop.Run()
349
  finally:
350
    server.Stop()
351

    
352

    
353
def Main():
354
  """Main function.
355

356
  """
357
  parser = optparse.OptionParser(description="Ganeti Remote API",
358
                                 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
359
                                 version="%%prog (ganeti) %s" %
360
                                 constants.RELEASE_VERSION)
361

    
362
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
363
                     default_ssl_cert=constants.RAPI_CERT_FILE,
364
                     default_ssl_key=constants.RAPI_CERT_FILE)