Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 073c31a5

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
    # Class '...' has no 'IN_...' member, pylint: disable-msg=E1103
257
    mask = (pyinotify.EventsCodes.IN_CLOSE_WRITE |
258
            pyinotify.EventsCodes.IN_DELETE |
259
            pyinotify.EventsCodes.IN_MOVED_FROM |
260
            pyinotify.EventsCodes.IN_MOVED_TO)
261

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

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

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

    
272

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

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

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

    
286

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

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

    
296
  ssconf.CheckMaster(options.debug)
297

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

    
305

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

    
309
  """
310

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

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

    
321
  server.LoadUsers(constants.RAPI_USERS_FILE)
322

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

    
327
  return (mainloop, server)
328

    
329

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

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

    
340

    
341
def main():
342
  """Main function.
343

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

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

    
353

    
354
if __name__ == "__main__":
355
  main()