Statistics
| Branch: | Tag: | Revision:

root / lib / server / rapi.py @ 84ad6b78

History | View | Annotate | Download (10.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013 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
from ganeti.rapi import baserlib
53

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

    
57

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

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

    
68

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

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

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

79
    @type user_fn: callable
80
    @param user_fn: Function receiving username as string and returning
81
      L{http.auth.PasswordFileUser} or C{None} if user is not found
82
    @type reqauth: bool
83
    @param reqauth: Whether to require authentication
84

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

    
95
  @staticmethod
96
  def FormatErrorMessage(values):
97
    """Formats the body of an error message.
98

99
    @type values: dict
100
    @param values: dictionary with keys C{code}, C{message} and C{explain}.
101
    @rtype: tuple; (string, string)
102
    @return: Content-type and response body
103

104
    """
105
    return (http.HTTP_APP_JSON, serializer.DumpJson(values))
106

    
107
  def _GetRequestContext(self, req):
108
    """Returns the context for a request.
109

110
    The context is cached in the req.private variable.
111

112
    """
113
    if req.private is None:
114
      (HandlerClass, items, args) = \
115
                     self._resmap.getController(req.request_path)
116

    
117
      ctx = RemoteApiRequestContext()
118
      ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
119

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

    
127
      ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method)
128

    
129
      # Require permissions definition (usually in the base class)
130
      if ctx.handler_access is None:
131
        raise AssertionError("Permissions definition missing")
132

    
133
      # This is only made available in HandleRequest
134
      ctx.body_data = None
135

    
136
      req.private = ctx
137

    
138
    # Check for expected attributes
139
    assert req.private.handler
140
    assert req.private.handler_fn
141
    assert req.private.handler_access is not None
142

    
143
    return req.private
144

    
145
  def AuthenticationRequired(self, req):
146
    """Determine whether authentication is required.
147

148
    """
149
    return self._reqauth or bool(self._GetRequestContext(req).handler_access)
150

    
151
  def Authenticate(self, req, username, password):
152
    """Checks whether a user can access a resource.
153

154
    """
155
    ctx = self._GetRequestContext(req)
156

    
157
    user = self._user_fn(username)
158
    if not (user and
159
            self.VerifyBasicAuthPassword(req, username, password,
160
                                         user.password)):
161
      # Unknown user or password wrong
162
      return False
163

    
164
    if (not ctx.handler_access or
165
        set(user.options).intersection(ctx.handler_access)):
166
      # Allow access
167
      return True
168

    
169
    # Access forbidden
170
    raise http.HttpForbidden()
171

    
172
  def HandleRequest(self, req):
173
    """Handles a request.
174

175
    """
176
    ctx = self._GetRequestContext(req)
177

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

    
189
      try:
190
        ctx.body_data = serializer.LoadJson(req.request_body)
191
      except Exception:
192
        raise http.HttpBadRequest(message="Unable to parse JSON data")
193
    else:
194
      ctx.body_data = None
195

    
196
    try:
197
      result = ctx.handler_fn()
198
    except luxi.TimeoutError:
199
      raise http.HttpGatewayTimeout()
200
    except luxi.ProtocolError, err:
201
      raise http.HttpBadGateway(str(err))
202

    
203
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
204

    
205
    return serializer.DumpJson(result)
206

    
207

    
208
class RapiUsers:
209
  def __init__(self):
210
    """Initializes this class.
211

212
    """
213
    self._users = None
214

    
215
  def Get(self, username):
216
    """Checks whether a user exists.
217

218
    """
219
    if self._users:
220
      return self._users.get(username, None)
221
    else:
222
      return None
223

    
224
  def Load(self, filename):
225
    """Loads a file containing users and passwords.
226

227
    @type filename: string
228
    @param filename: Path to file
229

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

    
243
      users = http.auth.ParsePasswordFile(contents)
244

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

    
250
    self._users = users
251

    
252
    return True
253

    
254

    
255
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
256
  def __init__(self, wm, path, cb):
257
    """Initializes this class.
258

259
    @param wm: Inotify watch manager
260
    @type path: string
261
    @param path: File path
262
    @type cb: callable
263
    @param cb: Function called on file change
264

265
    """
266
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
267

    
268
    self._cb = cb
269
    self._filename = os.path.basename(path)
270

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

    
278
    self._handle = self.AddWatch(os.path.dirname(path), mask)
279

    
280
  def process_default(self, event):
281
    """Called upon inotify event.
282

283
    """
284
    if event.name == self._filename:
285
      logging.debug("Received inotify event %s", event)
286
      self._cb()
287

    
288

    
289
def SetupFileWatcher(filename, cb):
290
  """Configures an inotify watcher for a file.
291

292
  @type filename: string
293
  @param filename: File to watch
294
  @type cb: callable
295
  @param cb: Function called on file change
296

297
  """
298
  wm = pyinotify.WatchManager()
299
  handler = FileEventHandler(wm, filename, cb)
300
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
301

    
302

    
303
def CheckRapi(options, args):
304
  """Initial checks whether to run or exit with a failure.
305

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

    
312
  ssconf.CheckMaster(options.debug)
313

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

    
321

    
322
def PrepRapi(options, _):
323
  """Prep remote API function, executed with the PID file held.
324

325
  """
326
  mainloop = daemon.Mainloop()
327

    
328
  users = RapiUsers()
329

    
330
  handler = RemoteApiHandler(users.Get, options.reqauth)
331

    
332
  # Setup file watcher (it'll be driven by asyncore)
333
  SetupFileWatcher(pathutils.RAPI_USERS_FILE,
334
                   compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
335

    
336
  users.Load(pathutils.RAPI_USERS_FILE)
337

    
338
  server = \
339
    http.server.HttpServer(mainloop, options.bind_address, options.port,
340
                           handler,
341
                           ssl_params=options.ssl_params, ssl_verify_peer=False)
342
  server.Start()
343

    
344
  return (mainloop, server)
345

    
346

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

350
  """
351
  (mainloop, server) = prep_data
352
  try:
353
    mainloop.Run()
354
  finally:
355
    server.Stop()
356

    
357

    
358
def Main():
359
  """Main function.
360

361
  """
362
  parser = optparse.OptionParser(description="Ganeti Remote API",
363
                                 usage=("%prog [-f] [-d] [-p port] [-b ADDRESS]"
364
                                        " [-i INTERFACE]"),
365
                                 version="%%prog (ganeti) %s" %
366
                                 constants.RELEASE_VERSION)
367
  parser.add_option("--require-authentication", dest="reqauth",
368
                    default=False, action="store_true",
369
                    help=("Disable anonymous HTTP requests and require"
370
                          " authentication"))
371

    
372
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
373
                     default_ssl_cert=pathutils.RAPI_CERT_FILE,
374
                     default_ssl_key=pathutils.RAPI_CERT_FILE)