Statistics
| Branch: | Tag: | Revision:

root / lib / server / rapi.py @ ec5af888

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 import pathutils
51
from ganeti.rapi import connector
52

    
53
import ganeti.http.auth   # pylint: disable=W0611
54
import ganeti.http.server
55

    
56

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

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

    
67

    
68
class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
69
                       http.server.HttpServerHandler):
70
  """REST Request Handler Class.
71

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

    
75
  def __init__(self, user_fn, _client_cls=None):
76
    """Initializes this class.
77

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

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

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

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

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

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

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

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

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

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

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

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

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

    
132
      req.private = ctx
133

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

    
139
    return req.private
140

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

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

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

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

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

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

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

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

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

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

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

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

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

    
201
    return serializer.DumpJson(result)
202

    
203

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

208
    """
209
    self._users = None
210

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

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

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

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

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

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

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

    
246
    self._users = users
247

    
248
    return True
249

    
250

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

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

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

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

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

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

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

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

    
284

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

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

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

    
298

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

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

    
308
  ssconf.CheckMaster(options.debug)
309

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

    
317

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

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

    
324
  users = RapiUsers()
325

    
326
  handler = RemoteApiHandler(users.Get)
327

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

    
332
  users.Load(pathutils.RAPI_USERS_FILE)
333

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

    
340
  return (mainloop, server)
341

    
342

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

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

    
353

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

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

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